diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6caaa1782adf..d9d7ad29b0f6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -30,6 +30,7 @@ /apps/icelandic-names-registry*/ @island-is/juni /apps/web/screens/PetitionView/ @island-is/juni /libs/cms/ @island-is/juni @island-is/stefna +/libs/clients/cms/ @island-is/juni @island-is/aranja /libs/residence-history @island-is/juni /libs/api/domains/endorsement-system @island-is/juni /libs/api/domains/icelandic-names-registry/ @island-is/juni @@ -130,6 +131,9 @@ codemagic.yaml /apps/portals/my-pages*/ @island-is/hugsmidjan /apps/services/regulations-admin-backend/ @island-is/hugsmidjan /apps/services/user-profile/ @island-is/hugsmidjan @island-is/juni @island-is/aranja +/apps/web/components/Grant/ @island-is/hugsmidjan +/apps/web/components/PlazaCard/ @island-is/hugsmidjan +/apps/web/screens/Grants/ @island-is/hugsmidjan /apps/web/screens/Regulations/ @island-is/hugsmidjan /apps/web/components/Regulations/ @island-is/hugsmidjan /apps/web/screens/OfficialJournalOfIceland/ @island-is/hugsmidjan @@ -194,7 +198,8 @@ codemagic.yaml /libs/portals/admin/regulations-admin/ @island-is/hugsmidjan /libs/portals/admin/document-provider/ @island-is/hugsmidjan @island-is/core /libs/clients/icelandic-health-insurance/rights-portal/ @island-is/hugsmidjan -/libs/clients/health-directorate @island-is/hugsmidjan @island-is/origo +/libs/clients/health-directorate @island-is/hugsmidjan +/libs/clients/health-directorate/src/lib/clients/occupational-license @island-is/hugsmidjan @island-is/origo /libs/clients/mms/grade @island-is/hugsmidjan /libs/portals/admin/air-discount-scheme @island-is/hugsmidjan /libs/application/templates/official-journal-of-iceland/ @island-is/hugsmidjan @@ -229,6 +234,10 @@ codemagic.yaml /libs/application/template-api-modules/src/lib/modules/templates/general-fishing-license/ @island-is/norda /libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/ @island-is/norda /libs/shared/form-fields/ @island-is/norda @island-is/island-ui +/libs/clients/financial-statements-inao @island-is/norda +/libs/api/domains/financial-statements-inao/ @island-is/norda +/libs/application/templates/financial-statements-inao/ @island-is/norda +/libs/application/template-api-modules/src/lib/modules/templates/financial-statements-inao @island-is/norda /libs/portals/my-pages/applications/ @island-is/norda-applications /libs/portals/admin/application-system/ @island-is/norda-applications @@ -310,10 +319,6 @@ codemagic.yaml /apps/services/auth/personal-representative/ @island-is/programm @island-is/aranja /apps/services/auth/personal-representative-public/ @island-is/programm @island-is/aranja -/libs/api/domains/financial-statements-inao/ @island-is/programm -/libs/clients/financial-statements-inao @island-is/programm -/libs/application/templates/financial-statements-inao/ @island-is/programm -/libs/application/template-api-modules/src/lib/modules/templates/financial-statements-inao @island-is/programm /libs/application/templates/criminal-record/ @island-is/origo /libs/application/template-api-modules/src/lib/modules/templates/criminal-record-submission/ @island-is/origo @@ -388,7 +393,7 @@ codemagic.yaml # QA /apps/system-e2e/ @island-is/qa -/libs/testing/e2e @island-is/qa +/libs/testing/e2e @island-is/qa # Islandis /apps/system-e2e/src/tests/islandis/admin-portal/ @island-is/aranja diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 51e54f62e853..71eace37971b 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -178,7 +178,7 @@ jobs: runs-on: ec2-runners container: image: public.ecr.aws/m3u4c4h9/island-is/actions-runner-public:latest - timeout-minutes: 35 + timeout-minutes: 45 env: AFFECTED_PROJECTS: ${{ matrix.projects }} MAX_JOBS: 1 @@ -220,7 +220,7 @@ jobs: runs-on: ec2-runners container: image: public.ecr.aws/m3u4c4h9/island-is/actions-runner-public:latest - timeout-minutes: 35 + timeout-minutes: 45 env: AFFECTED_PROJECT: ${{ matrix.projects }} CYPRESS_PROJECT_ID: 4q7jz8 @@ -288,8 +288,7 @@ jobs: with: github_token: ${{ secrets.github_token }} reporter: github-pr-review - fail_on_error: true - level: info + fail-level: info exclude: >- */node_modules/* diff --git a/.github/workflows/revert-pr.yaml b/.github/workflows/revert-pr.yaml new file mode 100644 index 000000000000..492ca4300e92 --- /dev/null +++ b/.github/workflows/revert-pr.yaml @@ -0,0 +1,105 @@ +name: Checksuit + +on: + workflow_run: + workflows: + - Monorepo pipeline - build and deploy + types: + - completed + branches: + - main + pull_request: + types: + - opened + - synchronize + - labeled + +permissions: + contents: write + pull-requests: write + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + detect-failure: + name: Determine Failure + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + runs-on: ec2-runners + container: + image: public.ecr.aws/m3u4c4h9/island-is/actions-runner-public:latest + outputs: + PR_NUMBER: ${{ steps.fetch-pr.outputs.PR_NUMBER }} + steps: + - uses: actions/checkout@v4 + + - name: Debug + run: | + echo "Conclusion: ${{ github.event.workflow_run.conclusion }}" + + - name: Fetch PR + id: fetch-pr + run: | + COMMIT_SHA="${{ github.event.workflow_run.head_commit.id }}" + if ! PR_NUMBER=$(bash ./scripts/ci/get-pr.sh "$COMMIT_SHA"); then + echo "Error: Failed to get PR number for commit $COMMIT_SHA" + exit 1 + fi + + if [ -n "$PR_NUMBER" ]; then + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT + else + echo "No merged PR found for commit $COMMIT_SHA." + fi + + - name: Add Revert Label + id: add-revert-label + if: ${{ steps.fetch-pr.outputs.PR_NUMBER && github.event.workflow_run.conclusion == 'failure' }} + run: | + PR_NUMBER="${{ steps.fetch-pr.outputs.PR_NUMBER }}" + if [ -z "$PR_NUMBER" ]; then + echo "Error: PR number is empty" + exit 1 + fi + echo "Adding revert label to PR: $PR_NUMBER" + if ! gh pr edit "$PR_NUMBER" --add-label "revert"; then + echo "Error: Failed to add revert label" + exit 1 + fi + + create-revert-pr: + name: Automated PR revert + needs: detect-failure + if: ${{ needs.detect-failure.outputs.PR_NUMBER && github.event.workflow_run.conclusion == 'failure' }} + runs-on: ec2-runners + container: + image: public.ecr.aws/m3u4c4h9/island-is/actions-runner-public:latest + steps: + - name: Debug + run: | + PR_NUMBER=${{ needs.detect-failure.outputs.PR_NUMBER }} + echo "Reverting PR: $PR_NUMBER" + + - name: Execute PR revert + if: ${{ vars.ENABLE_PR_REVERT == 'true' }} + run: | + REVERT_PR=$(bash ./scripts/ci/revert-pr.sh "$PR_NUMBER") + gh pr edit $REVERT_PR --add-label "automerge,high priority" + + manual-revert-pr: + name: Manual PR revert + runs-on: ec2-runners + container: + image: public.ecr.aws/m3u4c4h9/island-is/actions-runner-public:latest + if: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'revert') && (github.event.pull_request.merged == true || github.event.pull_request.draft == true) && github.actor != 'github-actions' }} + steps: + - name: Debug + run: | + echo "actor: ${{ github.actor }}" + echo "Reverting PR: ${{ github.event.pull_request.number }}" + + - name: Execute PR revert + if: ${{ vars.ENABLE_PR_REVERT == 'true' }} + run: | + REVERT_PR=$(bash ./scripts/ci/revert-pr.sh "${{ github.event.pull_request.number }}") + gh pr edit $REVERT_PR --add-label "automerge,high priority" diff --git a/README.md b/README.md index f5c26c5c8cde..209cda2c4c78 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Run on whenever you check out a branch: ```bash yarn install -yarn infra install +(cd infra/ && yarn install) yarn codegen ``` diff --git a/apps/application-system/api/infra/application-system-api.ts b/apps/application-system/api/infra/application-system-api.ts index ce7360862365..e4c51330e256 100644 --- a/apps/application-system/api/infra/application-system-api.ts +++ b/apps/application-system/api/infra/application-system-api.ts @@ -346,11 +346,11 @@ export const serviceSetup = (services: { }) .db() .migrations() - .liveness('/liveness') - .readiness('/liveness') + .liveness({ path: '/liveness', initialDelaySeconds: 20 }) + .readiness({ path: '/liveness', initialDelaySeconds: 20 }) .resources({ - limits: { cpu: '400m', memory: '1024Mi' }, - requests: { cpu: '75m', memory: '512Mi' }, + limits: { cpu: '600m', memory: '1024Mi' }, + requests: { cpu: '200m', memory: '512Mi' }, }) .replicaCount({ default: 2, diff --git a/apps/application-system/api/src/app/modules/application/application.controller.ts b/apps/application-system/api/src/app/modules/application/application.controller.ts index 8440e256c614..3e70189aa46e 100644 --- a/apps/application-system/api/src/app/modules/application/application.controller.ts +++ b/apps/application-system/api/src/app/modules/application/application.controller.ts @@ -76,7 +76,10 @@ import { ApplicationResponseDto } from './dto/application.response.dto' import { PresignedUrlResponseDto } from './dto/presignedUrl.response.dto' import { AssignApplicationDto } from './dto/assignApplication.dto' import { verifyToken } from './utils/tokenUtils' -import { getApplicationLifecycle } from './utils/application' +import { + getApplicationLifecycle, + removeObjectWithKeyFromAnswers, +} from './utils/application' import { DecodedAssignmentToken } from './types' import { ApplicationAccessService } from './tools/applicationAccess.service' import { CurrentLocale } from './utils/currentLocale' @@ -870,6 +873,10 @@ export class ApplicationController { existingApplication.id, { attachments: omit(existingApplication.attachments, key), + answers: removeObjectWithKeyFromAnswers( + existingApplication.answers, + key, + ), }, ) diff --git a/apps/application-system/api/src/app/modules/application/e2e/payment/payment.service.spec.ts b/apps/application-system/api/src/app/modules/application/e2e/payment/payment.service.spec.ts index bf6fbab72b0b..c7ad01fe4c0e 100644 --- a/apps/application-system/api/src/app/modules/application/e2e/payment/payment.service.spec.ts +++ b/apps/application-system/api/src/app/modules/application/e2e/payment/payment.service.spec.ts @@ -42,6 +42,13 @@ class MockChargeFjsV2ClientService { chargeItemName: '1', priceAmount: 1, }, + { + performingOrgID: performingOrganizationID, + chargeType: '1', + chargeItemCode: 'asdf2', + chargeItemName: '2', + priceAmount: 2, + }, ], }) } @@ -97,12 +104,12 @@ describe('Payment Service', () => { it('should create a charge', async () => { const performingOrganizationID = '1' - const chargeItemCodes: string[] = ['asdf'] + const chargeItems = [{ code: 'asdf' }] const result = await service.createCharge( user, performingOrganizationID, - chargeItemCodes, + chargeItems, applicationId, undefined, ) @@ -112,12 +119,27 @@ describe('Payment Service', () => { it('should create a charge with multiple charge items', async () => { const performingOrganizationID = '1' - const chargeItemCodes: string[] = ['asdf', 'asdf'] + const chargeItems = [{ code: 'asdf' }, { code: 'asdf' }] + + const result = await service.createCharge( + user, + performingOrganizationID, + chargeItems, + applicationId, + undefined, + ) + + expect(result).toBeTruthy() + }) + + it('should create a charge with multiple charge items using quantity', async () => { + const performingOrganizationID = '1' + const chargeItems = [{ code: 'asdf', quantity: 3 }] const result = await service.createCharge( user, performingOrganizationID, - chargeItemCodes, + chargeItems, applicationId, undefined, ) @@ -127,13 +149,13 @@ describe('Payment Service', () => { it('should throw an error when charge item is not found', async () => { const performingOrganizationID = '1' - const chargeItemCodes: string[] = ['13'] + const chargeItems = [{ code: '13' }] await expect( service.createCharge( user, performingOrganizationID, - chargeItemCodes, + chargeItems, applicationId, undefined, ), @@ -142,12 +164,12 @@ describe('Payment Service', () => { it('should get a payment status', async () => { const performingOrganizationID = '1' - const chargeItemCodes: string[] = ['asdf', 'asdf'] + const chargeItems = [{ code: 'asdf' }, { code: 'asdf' }] const charge = await service.createCharge( user, performingOrganizationID, - chargeItemCodes, + chargeItems, applicationId, undefined, ) @@ -158,12 +180,12 @@ describe('Payment Service', () => { it('should get a fulfilled payment status', async () => { const performingOrganizationID = '1' - const chargeItemCodes: string[] = ['asdf', 'asdf'] + const chargeItems = [{ code: 'asdf' }, { code: 'asdf' }] const charge = await service.createCharge( user, performingOrganizationID, - chargeItemCodes, + chargeItems, applicationId, undefined, ) @@ -177,9 +199,43 @@ describe('Payment Service', () => { expect(result.fulfilled).toBe(true) }) + it('should find charge items using quantity', async () => { + const performingOrganizationID = '1' + const chargeItems = [ + { code: 'asdf', quantity: 2 }, + { code: 'asdf2', quantity: 3 }, + ] + + const catalogChargeItems = await service.findCatalogChargeItems( + performingOrganizationID, + chargeItems, + ) + + // make sure quantity is correct for item 1 (code 'asdf') + const catalogQuantityItem1 = catalogChargeItems.find( + ({ chargeItemCode }) => chargeItemCode === chargeItems[0].code, + )?.quantity + expect(catalogQuantityItem1).toBe(chargeItems[0].quantity) + + // make sure quantity is correct for item 2 (code 'asdf2') + const catalogQuantityItem2 = catalogChargeItems.find( + ({ chargeItemCode }) => chargeItemCode === chargeItems[1].code, + )?.quantity + expect(catalogQuantityItem2).toBe(chargeItems[1].quantity) + + // make sure total sum is correct + const catalogTotalQuantity = catalogChargeItems.reduce( + (sum, item) => sum + (item.quantity || 0), + 0, + ) + expect(catalogTotalQuantity).toBe( + chargeItems[0].quantity + chargeItems[1].quantity, + ) + }) + it('Should throw when payment exists and status is in progress.', async () => { const performingOrganizationID = '1' - const chargeItemCodes: string[] = ['asdf', 'asdf'] + const chargeItems = [{ code: 'asdf' }, { code: 'asdf' }] jest.spyOn(fjsClient, 'getChargeStatus').mockResolvedValueOnce({ statusResult: { @@ -192,13 +248,13 @@ describe('Payment Service', () => { }, }) - const chargeItems = await service.findChargeItems( + const catalogChargeItems = await service.findCatalogChargeItems( performingOrganizationID, - chargeItemCodes, + chargeItems, ) const payment = await service.createPaymentModel( - chargeItems, + catalogChargeItems, applicationId, performingOrganizationID, ) @@ -207,7 +263,7 @@ describe('Payment Service', () => { service.createCharge( user, performingOrganizationID, - chargeItemCodes, + chargeItems, applicationId, undefined, ), @@ -216,7 +272,7 @@ describe('Payment Service', () => { it('Should continue with a payment that exists and status with an unpaid status.', async () => { const performingOrganizationID = '1' - const chargeItemCodes: string[] = ['asdf', 'asdf'] + const chargeItems = [{ code: 'asdf' }, { code: 'asdf' }] const mock = jest.spyOn(fjsClient, 'getChargeStatus') @@ -233,13 +289,13 @@ describe('Payment Service', () => { }), ) - const chargeItems = await service.findChargeItems( + const catalogChargeItems = await service.findCatalogChargeItems( performingOrganizationID, - chargeItemCodes, + chargeItems, ) const payment = await service.createPaymentModel( - chargeItems, + catalogChargeItems, applicationId, performingOrganizationID, ) @@ -247,7 +303,7 @@ describe('Payment Service', () => { const charge = await service.createCharge( user, performingOrganizationID, - chargeItemCodes, + chargeItems, applicationId, undefined, ) @@ -257,7 +313,7 @@ describe('Payment Service', () => { it('Should not create a new charge and a payment when payment exists', async () => { const performingOrganizationID = '1' - const chargeItemCodes: string[] = ['asdf', 'asdf'] + const chargeItems = [{ code: 'asdf' }, { code: 'asdf' }] const mock = jest.spyOn(fjsClient, 'getChargeStatus') const createChargeSpy = jest.spyOn(fjsClient, 'createCharge') @@ -275,13 +331,13 @@ describe('Payment Service', () => { }), ) - const chargeItems = await service.findChargeItems( + const catalogChargeItems = await service.findCatalogChargeItems( performingOrganizationID, - chargeItemCodes, + chargeItems, ) const payment = await service.createPaymentModel( - chargeItems, + catalogChargeItems, applicationId, performingOrganizationID, ) @@ -289,7 +345,7 @@ describe('Payment Service', () => { const charge = await service.createCharge( user, performingOrganizationID, - chargeItemCodes, + chargeItems, applicationId, undefined, ) diff --git a/apps/application-system/api/src/app/modules/application/e2e/payment/payment.spec.ts b/apps/application-system/api/src/app/modules/application/e2e/payment/payment.spec.ts index 5e3af416821f..5139740da68d 100644 --- a/apps/application-system/api/src/app/modules/application/e2e/payment/payment.spec.ts +++ b/apps/application-system/api/src/app/modules/application/e2e/payment/payment.spec.ts @@ -58,7 +58,7 @@ class MockPaymentService { } } - async findChargeItems() { + async findCatalogChargeItems() { return [ { performingOrgID: faker.datatype.number(), diff --git a/apps/application-system/api/src/app/modules/application/utils/application.ts b/apps/application-system/api/src/app/modules/application/utils/application.ts index b9f1cc3d2cfa..cbaed22ec0b4 100644 --- a/apps/application-system/api/src/app/modules/application/utils/application.ts +++ b/apps/application-system/api/src/app/modules/application/utils/application.ts @@ -156,3 +156,105 @@ export const mockApplicationFromTypeId = ( status: ApplicationStatus.IN_PROGRESS, } } + +type RecordType = Record + +const MAX_DEPTH = 100 + +export const removeObjectWithKeyFromAnswers = ( + answers: object, + keyToRemove: string, + depth = 0, +): object => { + if (depth >= MAX_DEPTH) { + console.warn( + 'Maximum recursion depth reached while calling removeObjectWithKeyFromAnswers', + ) + return answers + } + // Handle arrays + if (Array.isArray(answers)) { + return cleanArray(answers, keyToRemove, depth) + } + + // Handle objects + if (isValidObject(answers)) { + return cleanObject(answers, keyToRemove, depth) + } + + return answers +} + +const cleanArray = ( + array: unknown[], + keyToRemove: string, + depth: number, +): unknown[] => { + const filteredArray = array.filter((item) => { + if (isValidObject(item)) { + return !containsKey(item, keyToRemove) + } + return item !== keyToRemove + }) + + return filteredArray.map((item) => { + if (isObject(item)) { + return removeObjectWithKeyFromAnswers(item, keyToRemove, depth + 1) + } + return item + }) +} + +const cleanObject = ( + obj: object, + keyToRemove: string, + depth: number, +): RecordType => { + return Object.entries(obj).reduce((acc, [field, value]) => { + if (isValidObject(value)) { + if (!Array.isArray(value) && containsKey(value, keyToRemove)) { + return acc + } + + const cleanedValue = removeObjectWithKeyFromAnswers( + value, + keyToRemove, + depth + 1, + ) + + // For arrays or objects with content, keep them + if (hasContent(cleanedValue)) { + acc[field] = cleanedValue + } + // Special case: keep empty arrays + else if (Array.isArray(value)) { + acc[field] = cleanedValue + } + return acc + } + + // Handle primitive values + if (value !== keyToRemove) { + acc[field] = value + } + return acc + }, {}) +} + +const isValidObject = (value: unknown): value is object => { + return value !== null && typeof value === 'object' +} + +const containsKey = (obj: object, key: string): boolean => { + return Object.values(obj).includes(key) +} + +const hasContent = (value: unknown): boolean => { + if (Array.isArray(value)) { + return value.length > 0 + } + if (isValidObject(value)) { + return Object.keys(value).length > 0 + } + return false +} diff --git a/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts b/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts index 2eafaf9a4fc3..1a17720b751b 100644 --- a/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts +++ b/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts @@ -3,6 +3,7 @@ import { getApplicationNameTranslationString, getApplicationStatisticsNameTranslationString, getPaymentStatusForAdmin, + removeObjectWithKeyFromAnswers, } from './application' import { createApplication, @@ -196,4 +197,208 @@ describe('Testing utility functions for applications', () => { }) }) }) + + describe('removeAttachmentFromAnswers', () => { + it('Should remove an object from an array that contains the given key and leave the array empty', () => { + const givenAnswers = { + documents: [ + { id: 'doc1', attachmentId: 'some-key-123', name: 'Document 1' }, + ], + } + const expectedAnswers = { + documents: [], + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should remove nested objects that contain the given key', () => { + const givenAnswers = { + section1: { + attachment: { id: 'some-key-123', name: 'Remove me' }, + otherData: 'keep this', + }, + section2: { + data: 'keep this too', + }, + } + const expectedAnswers = { + section1: { + otherData: 'keep this', + }, + section2: { + data: 'keep this too', + }, + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should remove an object from an array that contains the given key', () => { + const givenAnswers = { + documents: [ + { id: 'doc1', attachmentId: 'some-key-123', name: 'Document 1' }, + { id: 'doc2', attachmentId: 'keep-this-key', name: 'Document 2' }, + ], + } + const expectedAnswers = { + documents: [ + { id: 'doc2', attachmentId: 'keep-this-key', name: 'Document 2' }, + ], + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should remove nested objects that contain the given key', () => { + const givenAnswers = { + section1: { + attachment: { id: 'some-key-123', name: 'Remove me' }, + otherData: 'keep this', + }, + section2: { + data: 'keep this too', + }, + } + const expectedAnswers = { + section1: { + otherData: 'keep this', + }, + section2: { + data: 'keep this too', + }, + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should handle deeply nested arrays and objects', () => { + const givenAnswers = { + deepSection: { + documents: [ + { + files: [ + { id: 'file1', attachmentId: 'some-key-123' }, + { id: 'file2', attachmentId: 'keep-this' }, + ], + }, + { + files: [{ id: 'file3', attachmentId: 'also-keep-this' }], + }, + ], + }, + } + const expectedAnswers = { + deepSection: { + documents: [ + { + files: [{ id: 'file2', attachmentId: 'keep-this' }], + }, + { + files: [{ id: 'file3', attachmentId: 'also-keep-this' }], + }, + ], + }, + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should handle even more complex deeply nested arrays and objects', () => { + const givenAnswers = { + deepSection: { + someRandomProp: { data: 'Some data' }, + deeperSection: { + documents: [ + { + files: [ + { id: 'file1', attachmentId: 'some-key-123', nr: 77 }, + { id: 'file2', attachmentId: 'keep-this', nr: 55 }, + ], + }, + { + files: [{ id: 'file3', attachmentId: 'also-keep-this' }], + }, + ], + otherSection: { + nr: 100, + name: 'Some Name', + kids: [ + { kid: 'Some kid', phone: 1234567 }, + { kid: 'Some other kid', phone: 1234568 }, + ], + }, + }, + }, + } + + const expectedAnswers = { + deepSection: { + someRandomProp: { data: 'Some data' }, + deeperSection: { + documents: [ + { + files: [{ id: 'file2', attachmentId: 'keep-this', nr: 55 }], + }, + { + files: [{ id: 'file3', attachmentId: 'also-keep-this' }], + }, + ], + otherSection: { + nr: 100, + name: 'Some Name', + kids: [ + { kid: 'Some kid', phone: 1234567 }, + { kid: 'Some other kid', phone: 1234568 }, + ], + }, + }, + }, + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should return empty object when no answers provided', () => { + const givenAnswers = {} + const expectedAnswers = {} + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + }) }) diff --git a/apps/contentful-apps/components/EntryListSearch.tsx b/apps/contentful-apps/components/EntryListSearch.tsx new file mode 100644 index 000000000000..90efc883c5ab --- /dev/null +++ b/apps/contentful-apps/components/EntryListSearch.tsx @@ -0,0 +1,155 @@ +import { useRef, useState } from 'react' +import { useDebounce } from 'react-use' +import { + CollectionProp, + EntryProps, + KeyValueMap, + QueryOptions, +} from 'contentful-management' +import { + Box, + EntryCard, + Pagination, + Spinner, + Stack, + Text, + TextInput, +} from '@contentful/f36-components' +import { useCMA, useSDK } from '@contentful/react-apps-toolkit' + +import { DEFAULT_LOCALE } from '../constants' + +const SEARCH_DEBOUNCE_TIME_IN_MS = 300 + +interface EntryListSearchProps { + contentTypeId: string + contentTypeLabel: string + contentTypeTitleField: string + itemsPerPage?: number + onEntryClick?: (entry: EntryProps) => void + query?: QueryOptions +} + +export const EntryListSearch = ({ + itemsPerPage = 4, + contentTypeId, + contentTypeLabel, + contentTypeTitleField, + onEntryClick, + query, +}: EntryListSearchProps) => { + const sdk = useSDK() + const cma = useCMA() + + const searchValueRef = useRef('') + const [searchValue, setSearchValue] = useState('') + const [listItemResponse, setListItemResponse] = + useState>>() + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(0) + const pageRef = useRef(0) + const [counter, setCounter] = useState(0) + + const skip = itemsPerPage * page + + useDebounce( + async () => { + setLoading(true) + try { + const response = await cma.entry.getMany({ + query: { + content_type: contentTypeId, + limit: itemsPerPage, + skip, + [`fields.${contentTypeTitleField}[match]`]: searchValue, + 'sys.archivedAt[exists]': false, + ...query, + }, + }) + if ( + searchValueRef.current === searchValue && + pageRef.current === page + ) { + setListItemResponse(response) + } + } finally { + setLoading(false) + } + }, + SEARCH_DEBOUNCE_TIME_IN_MS, + [page, searchValue, counter], + ) + + return ( + + { + searchValueRef.current = ev.target.value + setSearchValue(ev.target.value) + setPage(0) + pageRef.current = 0 + }} + /> + + + + + + {listItemResponse?.items?.length > 0 && ( + <> + + + {listItemResponse.items.map((item) => ( + { + if (onEntryClick) { + onEntryClick(item) + return + } + + sdk.navigator + .openEntry(item.sys.id, { + slideIn: { waitForClose: true }, + }) + .then(() => { + setCounter((c) => c + 1) + }) + }} + /> + ))} + + + { + pageRef.current = newPage + setPage(newPage) + }} + /> + + )} + + {listItemResponse?.items?.length === 0 && ( + + No entry was found + + )} + + ) +} diff --git a/apps/contentful-apps/components/editors/lists/GenericListEditor/GenericListEditor.tsx b/apps/contentful-apps/components/editors/lists/GenericListEditor/GenericListEditor.tsx index 0740aff1d59d..2eac9ec947d2 100644 --- a/apps/contentful-apps/components/editors/lists/GenericListEditor/GenericListEditor.tsx +++ b/apps/contentful-apps/components/editors/lists/GenericListEditor/GenericListEditor.tsx @@ -1,27 +1,14 @@ -import { useMemo, useRef, useState } from 'react' -import { useDebounce } from 'react-use' -import { CollectionProp, EntryProps, KeyValueMap } from 'contentful-management' +import { useMemo, useState } from 'react' import dynamic from 'next/dynamic' import { EditorExtensionSDK } from '@contentful/app-sdk' -import { - Box, - Button, - EntryCard, - FormControl, - Pagination, - Spinner, - Stack, - Text, - TextInput, -} from '@contentful/f36-components' +import { Box, Button, FormControl } from '@contentful/f36-components' import { PlusIcon } from '@contentful/f36-icons' import { useCMA, useSDK } from '@contentful/react-apps-toolkit' +import { EntryListSearch } from '../../../../components/EntryListSearch' import { mapLocalesToFieldApis } from '../../utils' -const SEARCH_DEBOUNCE_TIME_IN_MS = 300 const LIST_ITEM_CONTENT_TYPE_ID = 'genericListItem' -const LIST_ITEMS_PER_PAGE = 4 const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { return { @@ -58,51 +45,11 @@ export const GenericListEditor = () => { const sdk = useSDK() const cma = useCMA() - const searchValueRef = useRef('') - const [searchValue, setSearchValue] = useState('') - const [listItemResponse, setListItemResponse] = - useState>>() - const [loading, setLoading] = useState(false) - const [page, setPage] = useState(0) - const pageRef = useRef(0) - /** Counter that's simply used to refresh the list when an item gets created */ - const [counter, setCounter] = useState(0) - - const skip = LIST_ITEMS_PER_PAGE * page + const [_, setCounter] = useState(0) const defaultLocale = sdk.locales.default - useDebounce( - async () => { - setLoading(true) - try { - const response = await cma.entry.getMany({ - environmentId: sdk.ids.environment, - spaceId: sdk.ids.space, - query: { - content_type: LIST_ITEM_CONTENT_TYPE_ID, - limit: LIST_ITEMS_PER_PAGE, - skip, - 'fields.internalTitle[match]': searchValue, - 'fields.genericList.sys.id': sdk.entry.getSys().id, - 'sys.archivedAt[exists]': false, - }, - }) - if ( - searchValueRef.current === searchValue && - pageRef.current === page - ) { - setListItemResponse(response) - } - } finally { - setLoading(false) - } - }, - SEARCH_DEBOUNCE_TIME_IN_MS, - [page, searchValue, counter], - ) - const createListItem = async () => { const cardIntro = {} @@ -189,70 +136,14 @@ export const GenericListEditor = () => { - - { - searchValueRef.current = ev.target.value - setSearchValue(ev.target.value) - setPage(0) - pageRef.current = 0 - }} - /> - - - - - - {listItemResponse?.items?.length > 0 && ( - <> - - - {listItemResponse.items.map((item) => ( - { - sdk.navigator - .openEntry(item.sys.id, { - slideIn: { waitForClose: true }, - }) - .then(() => { - setCounter((c) => c + 1) - }) - }} - /> - ))} - - - { - pageRef.current = newPage - setPage(newPage) - }} - /> - - )} - - {listItemResponse?.items?.length === 0 && ( - - No item was found - - )} - + ) } diff --git a/apps/contentful-apps/pages/fields/chart-component-source-data-key-field.tsx b/apps/contentful-apps/pages/fields/chart-component-source-data-key-field.tsx index 37c4c7f67214..16b4614e49b5 100644 --- a/apps/contentful-apps/pages/fields/chart-component-source-data-key-field.tsx +++ b/apps/contentful-apps/pages/fields/chart-component-source-data-key-field.tsx @@ -78,6 +78,7 @@ const ChartComponentSourceDataKeyField = () => { sdk.entry.fields.values.getValue() || { typeOfSource: SourceDataKeyValues.ExternalSourceKey, typeOfManualDataKey: ManualDataKeyValues.Date, + externalSourceDataKey: sdk.entry.fields.sourceDataKey.getValue(), dateItems: [], categoryItems: [], }, diff --git a/apps/contentful-apps/pages/fields/generic-tag-group-items-field.tsx b/apps/contentful-apps/pages/fields/generic-tag-group-items-field.tsx index 6a577e389b9d..bdd4a8f9a83e 100644 --- a/apps/contentful-apps/pages/fields/generic-tag-group-items-field.tsx +++ b/apps/contentful-apps/pages/fields/generic-tag-group-items-field.tsx @@ -1,37 +1,17 @@ -import { useEffect, useRef, useState } from 'react' -import { useDebounce } from 'react-use' +import { useEffect, useState } from 'react' import type { FieldExtensionSDK } from '@contentful/app-sdk' -import { - Box, - Button, - EntryCard, - Pagination, - Spinner, - Stack, - Text, - TextInput, -} from '@contentful/f36-components' +import { Box, Button } from '@contentful/f36-components' import { PlusIcon } from '@contentful/f36-icons' import { useCMA, useSDK } from '@contentful/react-apps-toolkit' -const LIST_ITEMS_PER_PAGE = 4 -const SEARCH_DEBOUNCE_TIME_IN_MS = 300 +import { EntryListSearch } from '../../components/EntryListSearch' const GenericTagGroupItemsField = () => { - const [page, setPage] = useState(0) - const pageRef = useRef(0) - const [searchValue, setSearchValue] = useState('') - const searchValueRef = useRef('') - const [listItemResponse, setListItemResponse] = useState(null) - const [isLoading, setIsLoading] = useState(false) - - const [counter, setCounter] = useState(0) + const [_, setCounter] = useState(0) const sdk = useSDK() const cma = useCMA() - const skip = LIST_ITEMS_PER_PAGE * page - const createGenericTag = async () => { const tag = await cma.entry.create( { @@ -62,35 +42,6 @@ const GenericTagGroupItemsField = () => { }) } - useDebounce( - async () => { - setIsLoading(true) - try { - const response = await cma.entry.getMany({ - query: { - content_type: 'genericTag', - limit: LIST_ITEMS_PER_PAGE, - skip, - 'fields.internalTitle[match]': searchValue, - 'fields.genericTagGroup.sys.id': sdk.entry.getSys().id, - 'sys.archivedAt[exists]': false, - }, - }) - - if ( - searchValueRef.current === searchValue && - pageRef.current === page - ) { - setListItemResponse(response) - } - } finally { - setIsLoading(false) - } - }, - SEARCH_DEBOUNCE_TIME_IN_MS, - [page, searchValue, counter], - ) - useEffect(() => { sdk.window.startAutoResizer() return () => { @@ -108,71 +59,14 @@ const GenericTagGroupItemsField = () => { - - { - searchValueRef.current = ev.target.value - setSearchValue(ev.target.value) - setPage(0) - pageRef.current = 0 - }} - /> - - - - - - {listItemResponse?.items?.length > 0 && ( - <> - - - {listItemResponse.items.map((item) => ( - { - sdk.navigator - .openEntry(item.sys.id, { - slideIn: { waitForClose: true }, - }) - .then(() => { - setCounter((c) => c + 1) - }) - }} - /> - ))} - - - { - pageRef.current = newPage - setPage(newPage) - }} - /> - - )} - - {listItemResponse?.items?.length === 0 && ( - - No item was found - - )} - + ) } diff --git a/apps/judicial-system/README.md b/apps/judicial-system/README.md index f2ee7f505d90..2770e62b06f8 100644 --- a/apps/judicial-system/README.md +++ b/apps/judicial-system/README.md @@ -226,18 +226,6 @@ You can serve this service locally by running: yarn start judicial-system-xrd-api ``` -## Robot API - -This service is for access through xRoad. - -### Running locally - -You can serve this service locally by running: - -```bash -yarn start judicial-system-robot-api -``` - ## Digital Mailbox API This service is for access through xRoad. diff --git a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts index 5c76205ba8d7..f73423c0f6d4 100644 --- a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts +++ b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts @@ -731,6 +731,54 @@ describe('getIndictmentInfo', () => { indictmentVerdictAppealDeadlineExpired: false, }) }) + + it('should return correct indictment info when the indictment ruling decision is FINE and the appeal deadline is not expired', () => { + const rulingDate = new Date().toISOString() + const defendants = [ + { + serviceRequirement: ServiceRequirement.NOT_REQUIRED, + verdictViewDate: undefined, + } as Defendant, + ] + + const indictmentInfo = getIndictmentInfo( + CaseIndictmentRulingDecision.FINE, + rulingDate, + defendants, + ) + + expect(indictmentInfo).toEqual({ + indictmentAppealDeadline: new Date( + new Date(rulingDate).setDate(new Date(rulingDate).getDate() + 3), + ).toISOString(), + indictmentVerdictViewedByAll: true, + indictmentVerdictAppealDeadlineExpired: false, + }) + }) + + it('should return correct indictment info when the indictment ruling decision is FINE and the appeal deadline is expired', () => { + const rulingDate = '2024-05-26T21:51:19.156Z' + const defendants = [ + { + serviceRequirement: ServiceRequirement.NOT_REQUIRED, + verdictViewDate: undefined, + } as Defendant, + ] + + const indictmentInfo = getIndictmentInfo( + CaseIndictmentRulingDecision.FINE, + rulingDate, + defendants, + ) + + expect(indictmentInfo).toEqual({ + indictmentAppealDeadline: new Date( + new Date(rulingDate).setDate(new Date(rulingDate).getDate() + 3), + ).toISOString(), + indictmentVerdictViewedByAll: true, + indictmentVerdictAppealDeadlineExpired: true, + }) + }) }) describe('getIndictmentDefendantsInfo', () => { diff --git a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts index bf8139e14014..f01909d284f1 100644 --- a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts +++ b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts @@ -2,6 +2,7 @@ import { CaseAppealDecision, CaseIndictmentRulingDecision, EventType, + FINE_APPEAL_WINDOW_DAYS, getIndictmentVerdictAppealDeadlineStatus, getStatementDeadline, isRequestCase, @@ -151,6 +152,8 @@ export const getIndictmentInfo = ( eventLog?: EventLog[], ): IndictmentInfo => { const indictmentInfo: IndictmentInfo = {} + const isFine = rulingDecision === CaseIndictmentRulingDecision.FINE + const isRuling = rulingDecision === CaseIndictmentRulingDecision.RULING if (!rulingDate) { return indictmentInfo @@ -158,13 +161,16 @@ export const getIndictmentInfo = ( const theRulingDate = new Date(rulingDate) indictmentInfo.indictmentAppealDeadline = new Date( - theRulingDate.getTime() + getDays(VERDICT_APPEAL_WINDOW_DAYS), + theRulingDate.getTime() + + getDays(isFine ? FINE_APPEAL_WINDOW_DAYS : VERDICT_APPEAL_WINDOW_DAYS), ).toISOString() const verdictInfo = defendants?.map<[boolean, Date | undefined]>( (defendant) => [ - rulingDecision === CaseIndictmentRulingDecision.RULING, - defendant.serviceRequirement === ServiceRequirement.NOT_REQUIRED + isRuling || isFine, + isFine + ? theRulingDate + : defendant.serviceRequirement === ServiceRequirement.NOT_REQUIRED ? new Date() : defendant.verdictViewDate ? new Date(defendant.verdictViewDate) @@ -173,7 +179,7 @@ export const getIndictmentInfo = ( ) const [indictmentVerdictViewedByAll, indictmentVerdictAppealDeadlineExpired] = - getIndictmentVerdictAppealDeadlineStatus(verdictInfo) + getIndictmentVerdictAppealDeadlineStatus(verdictInfo, isFine) indictmentInfo.indictmentVerdictViewedByAll = indictmentVerdictViewedByAll indictmentInfo.indictmentVerdictAppealDeadlineExpired = indictmentVerdictAppealDeadlineExpired @@ -189,6 +195,8 @@ export const getIndictmentDefendantsInfo = (theCase: Case) => { return theCase.defendants?.map((defendant) => { const serviceRequired = defendant.serviceRequirement === ServiceRequirement.REQUIRED + const isFine = + theCase.indictmentRulingDecision === CaseIndictmentRulingDecision.FINE const { verdictViewDate } = defendant @@ -196,7 +204,10 @@ export const getIndictmentDefendantsInfo = (theCase: Case) => { const verdictAppealDeadline = baseDate ? new Date( - new Date(baseDate).getTime() + getDays(VERDICT_APPEAL_WINDOW_DAYS), + new Date(baseDate).getTime() + + getDays( + isFine ? FINE_APPEAL_WINDOW_DAYS : VERDICT_APPEAL_WINDOW_DAYS, + ), ).toISOString() : undefined 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 325210f11a3a..d25bad9175cb 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 @@ -109,4 +109,9 @@ export class UpdateDefendantInput { @IsOptional() @Field(() => Boolean, { nullable: true }) readonly caseFilesSharedWithDefender?: boolean + + @Allow() + @IsOptional() + @Field(() => Boolean, { nullable: true }) + readonly isSentToPrisonAdmin?: boolean } 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 4f641c32efe8..b5276f9bc104 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 @@ -101,4 +101,10 @@ export class Defendant { @Field(() => Boolean, { nullable: true }) readonly caseFilesSharedWithDefender?: boolean + + @Field(() => Boolean, { nullable: true }) + readonly isSentToPrisonAdmin?: boolean + + @Field(() => String, { nullable: true }) + readonly sentToPrisonAdminDate?: string } diff --git a/apps/judicial-system/backend/infra/judicial-system-backend.ts b/apps/judicial-system/backend/infra/judicial-system-backend.ts index aa29ca39050c..bab01682da23 100644 --- a/apps/judicial-system/backend/infra/judicial-system-backend.ts +++ b/apps/judicial-system/backend/infra/judicial-system-backend.ts @@ -70,6 +70,8 @@ export const serviceSetup = (): ServiceBuilder<'judicial-system-backend'> => EMAIL_REPLY_TO_NAME: '/k8s/judicial-system/EMAIL_REPLY_TO_NAME', PRISON_EMAIL: '/k8s/judicial-system/PRISON_EMAIL', PRISON_ADMIN_EMAIL: '/k8s/judicial-system/PRISON_ADMIN_EMAIL', + PRISON_ADMIN_INDICTMENT_EMAILS: + '/k8s/judicial-system/PRISON_ADMIN_INDICTMENT_EMAILS', AUTH_JWT_SECRET: '/k8s/judicial-system/AUTH_JWT_SECRET', ADMIN_USERS: '/k8s/judicial-system/ADMIN_USERS', BACKEND_ACCESS_TOKEN: '/k8s/judicial-system/BACKEND_ACCESS_TOKEN', diff --git a/apps/judicial-system/backend/migrations/20241112125245-update-defendant.js b/apps/judicial-system/backend/migrations/20241112125245-update-defendant.js new file mode 100644 index 000000000000..f10aab86673c --- /dev/null +++ b/apps/judicial-system/backend/migrations/20241112125245-update-defendant.js @@ -0,0 +1,25 @@ +'use strict' + +module.exports = { + up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction((transaction) => + queryInterface.addColumn( + 'defendant', + 'is_sent_to_prison_admin', + { + type: Sequelize.BOOLEAN, + allowNull: true, + }, + { transaction }, + ), + ) + }, + + down(queryInterface) { + return queryInterface.sequelize.transaction((transaction) => + queryInterface.removeColumn('defendant', 'is_sent_to_prison_admin', { + transaction, + }), + ) + }, +} diff --git a/apps/judicial-system/backend/migrations/20241114123528-create-defendant-event-log.js b/apps/judicial-system/backend/migrations/20241114123528-create-defendant-event-log.js new file mode 100644 index 000000000000..3903be153082 --- /dev/null +++ b/apps/judicial-system/backend/migrations/20241114123528-create-defendant-event-log.js @@ -0,0 +1,56 @@ +'use strict' + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => + queryInterface.createTable( + 'defendant_event_log', + { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4, + }, + created: { + type: 'TIMESTAMP WITH TIME ZONE', + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + allowNull: false, + }, + modified: { + type: 'TIMESTAMP WITH TIME ZONE', + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + allowNull: false, + }, + case_id: { + type: Sequelize.UUID, + references: { + model: 'case', + key: 'id', + }, + allowNull: false, + }, + defendant_id: { + type: Sequelize.UUID, + references: { + model: 'defendant', + key: 'id', + }, + allowNull: false, + }, + event_type: { + type: Sequelize.STRING, + allowNull: false, + }, + }, + { transaction: t }, + ), + ) + }, + + down: (queryInterface) => { + return queryInterface.sequelize.transaction((t) => + queryInterface.dropTable('defendant_event_log', { transaction: t }), + ) + }, +} diff --git a/apps/judicial-system/backend/migrations/20241121185031-strip-national-id-hyphen.js b/apps/judicial-system/backend/migrations/20241121185031-strip-national-id-hyphen.js new file mode 100644 index 000000000000..09878fb419e1 --- /dev/null +++ b/apps/judicial-system/backend/migrations/20241121185031-strip-national-id-hyphen.js @@ -0,0 +1,23 @@ +'use strict' + +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction((transaction) => + queryInterface.bulkUpdate( + 'defendant', + { + national_id: queryInterface.sequelize.literal( + `REPLACE(national_id, '-', '')`, + ), + }, + { national_id: { [Sequelize.Op.like]: '%-%' } }, + { transaction }, + ), + ) + }, + + async down() { + // Optional: Implement logic to revert the changes if necessary + return + }, +} 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 3379703684ae..15b4b0fe2d94 100644 --- a/apps/judicial-system/backend/src/app/formatters/formatters.spec.ts +++ b/apps/judicial-system/backend/src/app/formatters/formatters.spec.ts @@ -15,6 +15,7 @@ import { } from '@island.is/judicial-system/types' import { + filterWhitelistEmails, formatCourtHeadsUpSmsNotification, formatCourtReadyForCourtSmsNotification, formatCourtResubmittedToCourtSmsNotification, @@ -1982,3 +1983,38 @@ describe('formatDefenderResubmittedToCourtEmailNotification', () => { expect(result.subject).toEqual('Krafa í máli R-2022/999') }) }) + +describe('filterWhitelistEmails', () => { + const emails = [ + 'test@rvg.is', + 'test2@rvg.is', + 'test3@rvg.is', + 'test4@example.com', + ] + + it('should return only whitelisted emails', () => { + const whitelist = `${emails[0]}, ${emails[2]}` + const domainWhitelist = 'example.com' + + const result = filterWhitelistEmails(emails, domainWhitelist, whitelist) + + expect(result).toEqual([emails[0], emails[2], emails[3]]) + }) + + it('should return empty array if no emails are whitelisted', () => { + const whitelist = '' + const domainWhitelist = '' + + const result = filterWhitelistEmails(emails, domainWhitelist, whitelist) + + expect(result).toEqual([]) + }) + it('should return domain whitelisted emails', () => { + const whitelist = '' + const domainWhitelist = 'rvg.is' + + const result = filterWhitelistEmails(emails, domainWhitelist, whitelist) + + expect(result).toEqual([emails[0], emails[1], emails[2]]) + }) +}) diff --git a/apps/judicial-system/backend/src/app/formatters/formatters.ts b/apps/judicial-system/backend/src/app/formatters/formatters.ts index d48904e27885..0f43edf7200b 100644 --- a/apps/judicial-system/backend/src/app/formatters/formatters.ts +++ b/apps/judicial-system/backend/src/app/formatters/formatters.ts @@ -5,7 +5,6 @@ import { DEFENDER_ROUTE, } from '@island.is/judicial-system/consts' import { - capitalize, enumerate, formatCaseType, formatDate, @@ -14,11 +13,7 @@ import { laws, readableIndictmentSubtypes, } from '@island.is/judicial-system/formatters' -import { - AdvocateType, - Gender, - UserRole, -} from '@island.is/judicial-system/types' +import { Gender, UserRole } from '@island.is/judicial-system/types' import { CaseCustodyRestrictions, CaseLegalProvisions, @@ -668,49 +663,6 @@ export const formatCustodyRestrictions = ( }) } -export const formatAdvocateAssignedEmailNotification = ( - formatMessage: FormatMessage, - theCase: Case, - advocateType: AdvocateType, - overviewUrl?: string, -): SubjectAndBody => { - const subject = - advocateType === AdvocateType.DEFENDER - ? formatMessage( - notifications.advocateAssignedEmail.subjectAccessToCaseFiles, - { - court: capitalize(theCase.court?.name ?? ''), - }, - ) - : formatMessage(notifications.advocateAssignedEmail.subjectAccess, { - courtCaseNumber: theCase.courtCaseNumber, - }) - - const body = - advocateType === AdvocateType.DEFENDER - ? formatMessage( - notifications.advocateAssignedEmail.bodyAccessToCaseFiles, - { - defenderHasAccessToRVG: Boolean(overviewUrl), - courtCaseNumber: capitalize(theCase.courtCaseNumber ?? ''), - court: theCase.court?.name ?? '', - courtName: theCase.court?.name.replace('dómur', 'dómi') ?? '', - linkStart: ``, - linkEnd: '', - }, - ) - : formatMessage(notifications.advocateAssignedEmail.bodyAccess, { - defenderHasAccessToRVG: Boolean(overviewUrl), - court: theCase.court?.name, - advocateType, - courtCaseNumber: capitalize(theCase.courtCaseNumber ?? ''), - linkStart: ``, - linkEnd: '', - }) - - return { body, subject } -} - export const formatCourtOfAppealJudgeAssignedEmailNotification = ( formatMessage: FormatMessage, caseNumber: string, @@ -802,3 +754,31 @@ export const formatDefenderRoute = ( export const formatConfirmedIndictmentKey = (key?: string) => key?.replace(/\/([^/]*)$/, '/confirmed/$1') ?? '' + +export const filterWhitelistEmails = ( + emails: string[], + domainWhitelist: string, + emailWhitelist: string, +) => { + if (!emails || emails.length === 0) return [] + + const allowedDomains = new Set( + domainWhitelist + .split(',') + .map((d) => d.trim()) + .filter(Boolean), + ) + const allowedEmails = new Set( + emailWhitelist + .split(',') + .map((e) => e.trim()) + .filter(Boolean), + ) + + return emails.filter((email) => { + const domain = email.split('@')[1] + return ( + domain && (allowedDomains.has(domain) || allowedEmails.has(email.trim())) + ) + }) +} diff --git a/apps/judicial-system/backend/src/app/formatters/index.ts b/apps/judicial-system/backend/src/app/formatters/index.ts index 48f8af67b53e..01d46156bf88 100644 --- a/apps/judicial-system/backend/src/app/formatters/index.ts +++ b/apps/judicial-system/backend/src/app/formatters/index.ts @@ -21,13 +21,13 @@ export { formatProsecutorReceivedByCourtSmsNotification, formatDefenderCourtDateLinkEmailNotification, formatDefenderResubmittedToCourtEmailNotification, - formatAdvocateAssignedEmailNotification, formatCourtIndictmentReadyForCourtEmailNotification, formatDefenderRoute, formatDefenderReadyForCourtEmailNotification, formatCourtOfAppealJudgeAssignedEmailNotification, formatPostponedCourtDateEmailNotification, stripHtmlTags, + filterWhitelistEmails, } from './formatters' export { Confirmation } from './pdfHelpers' export { getRequestPdfAsBuffer, getRequestPdfAsString } from './requestPdf' diff --git a/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts b/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts index 0279dbb208f9..f6ac73e56483 100644 --- a/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts @@ -113,16 +113,13 @@ export const createIndictment = async ( addEmptyLines(doc, 2) addNormalPlusCenteredText( doc, - formatMessage( - indictment.signature, - { - prosecutorsOfficeName: - lowercase(theCase.prosecutorsOffice?.name) - .replace('lögreglustjórinn', 'lögreglustjórans') - .replace('saksóknari', 'saksóknara') ?? '', - date: formatDate(nowFactory(), 'PPP'), - } ?? '', - ), + formatMessage(indictment.signature, { + prosecutorsOfficeName: + lowercase(theCase.prosecutorsOffice?.name) + .replace('lögreglustjórinn', 'lögreglustjórans') + .replace('saksóknari', 'saksóknara') ?? '', + date: formatDate(nowFactory(), 'PPP'), + }), ) doc.end() diff --git a/apps/judicial-system/backend/src/app/messages/notifications.ts b/apps/judicial-system/backend/src/app/messages/notifications.ts index fe12aea5011d..a7bf8f898acf 100644 --- a/apps/judicial-system/backend/src/app/messages/notifications.ts +++ b/apps/judicial-system/backend/src/app/messages/notifications.ts @@ -45,6 +45,16 @@ export const notifications = { 'Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.', description: 'Notaður sem texti í email til þess að tilgreina slóð á RVG', }), + emailWhitelist: defineMessage({ + id: 'judicial.system.backend:notifications.email_whitelist', + defaultMessage: '', + description: 'Notað til að tilgreina hvort póstfang sé í hvítlista', + }), + emailWhitelistDomains: defineMessage({ + id: 'judicial.system.backend:notifications.email_whitelist_domains', + defaultMessage: 'omnitrix.is,kolibri.is', + description: 'Notað til að tilgreina hvort póstfang sé í hvítlista', + }), readyForCourt: defineMessages({ subject: { id: 'judicial.system.backend:notifications.ready_for_court.subjectV2', @@ -607,32 +617,6 @@ export const notifications = { 'Notaður sem texti í tölvupósti til verjanda vegna breytingar á lengd gæslu/einangrunar/vistunar þar sem úrskurðað var í einangrun.', }, }), - advocateAssignedEmail: defineMessages({ - subjectAccessToCaseFiles: { - id: 'judicial.system.backend:notifications.defender_assigned_email.subject_access_to_case_files', - defaultMessage: '{court} - aðgangur að málsgögnum', - description: - 'Fyrirsögn í pósti til verjanda þegar hann er skráður á mál.', - }, - subjectAccess: { - id: 'judicial.system.backend:notifications.defender_assigned_email.subject_access', - defaultMessage: 'Skráning í máli {courtCaseNumber}', - description: - 'Fyrirsögn í pósti til verjanda þegar hann er skráður á mál.', - }, - bodyAccessToCaseFiles: { - id: 'judicial.system.backend:notifications.defender_assigned_email.body_access_to_case_files', - defaultMessage: - '{court} hefur skráð þig verjanda í máli {courtCaseNumber}.

{defenderHasAccessToRVG, select, true {Gögn málsins eru aðgengileg á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast gögn málsins hjá {courtName} ef þau hafa ekki þegar verið afhent}}.', - description: 'Texti í pósti til verjanda þegar hann er skráður á mál.', - }, - bodyAccess: { - id: 'judicial.system.backend:notifications.defender_assigned_email.body_access', - defaultMessage: - '{court} hefur skráð þig {advocateType, select, LAWYER {lögmann einkaréttarkröfuhafa} LEGAL_RIGHTS_PROTECTOR {réttargæslumann einkaréttarkröfuhafa} other {verjanda}} í máli {courtCaseNumber}.

{defenderHasAccessToRVG, select, true {Sjá nánar á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast málið hjá {courtName}.}}.', - description: 'Texti í pósti til verjanda þegar hann er skráður á mál.', - }, - }), defendantsNotUpdatedAtCourt: defineMessages({ subject: { id: 'judicial.system.backend:notifications.defendants_not_updated_at_court.subject', 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 cbab6e505062..63b82483e3a9 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 @@ -39,14 +39,10 @@ import { } from '@island.is/judicial-system/formatters' import type { User } from '@island.is/judicial-system/types' import { - CaseAppealRulingDecision, - CaseDecision, CaseState, - CaseTransition, CaseType, indictmentCases, investigationCases, - isRestrictionCase, restrictionCases, UserRole, } from '@island.is/judicial-system/types' 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 6f77a6752c3e..9a671f3ba761 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 @@ -40,6 +40,7 @@ import { CaseType, DateType, dateTypes, + defendantEventTypes, EventType, eventTypes, isCompletedCase, @@ -59,7 +60,12 @@ import { } from '../../formatters' import { AwsS3Service } from '../aws-s3' import { CourtService } from '../court' -import { CivilClaimant, Defendant, DefendantService } from '../defendant' +import { + CivilClaimant, + Defendant, + DefendantEventLog, + DefendantService, +} from '../defendant' import { EventService } from '../event' import { EventLog, EventLogService } from '../event-log' import { CaseFile, FileService } from '../file' @@ -177,13 +183,13 @@ export interface UpdateCase courtRecordSignatoryId?: string | null courtRecordSignatureDate?: Date | null parentCaseId?: string | null - arraignmentDate?: UpdateDateLog | null - courtDate?: UpdateDateLog | null - postponedIndefinitelyExplanation?: string | null indictmentReturnedExplanation?: string | null indictmentDeniedExplanation?: string | null indictmentHash?: string | null - civilDemands?: string | null + arraignmentDate?: UpdateDateLog + courtDate?: UpdateDateLog + postponedIndefinitelyExplanation?: string + civilDemands?: string } type DateLogKeys = keyof Pick @@ -285,6 +291,14 @@ export const include: Includeable[] = [ order: [['created', 'DESC']], separate: true, }, + { + model: DefendantEventLog, + as: 'eventLogs', + required: false, + where: { eventType: defendantEventTypes }, + order: [['created', 'DESC']], + separate: true, + }, ], separate: true, }, @@ -784,10 +798,9 @@ export class CaseService { elementId: caseFile.id, })) ?? [] - const messages = this.getDeliverProsecutorToCourtMessages(theCase, user) - .concat(this.getDeliverDefendantToCourtMessages(theCase, user)) - .concat(deliverCaseFilesRecordToCourtMessages) - .concat(deliverCaseFileToCourtMessages) + const messages: Message[] = deliverCaseFilesRecordToCourtMessages.concat( + deliverCaseFileToCourtMessages, + ) if (isTrafficViolationCase(theCase)) { messages.push({ @@ -2035,11 +2048,23 @@ export class CaseService { } async createCourtCase(theCase: Case, user: TUser): Promise { + let receivalDate: Date + + if (isIndictmentCase(theCase.type)) { + receivalDate = + theCase.eventLogs?.find( + (eventLog) => eventLog.eventType === EventType.INDICTMENT_CONFIRMED, + )?.created ?? nowFactory() + } else { + receivalDate = nowFactory() + } + const courtCaseNumber = await this.courtService.createCourtCase( user, theCase.id, theCase.courtId, theCase.type, + receivalDate, theCase.policeCaseNumbers, Boolean(theCase.parentCaseId), theCase.indictmentSubtypes, diff --git a/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts b/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts index f3f920723a31..88e884019b39 100644 --- a/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts @@ -1,3 +1,4 @@ +import { Transform } from 'class-transformer' import { ArrayMinSize, IsArray, @@ -6,6 +7,8 @@ import { IsObject, IsOptional, IsString, + MaxLength, + MinLength, } from 'class-validator' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' @@ -19,6 +22,8 @@ import { RequestSharedWithDefender, } from '@island.is/judicial-system/types' +import { nationalIdTransformer } from '../../../transformers' + export class CreateCaseDto { @IsNotEmpty() @IsEnum(CaseType) @@ -32,6 +37,7 @@ export class CreateCaseDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly description?: string @@ -39,26 +45,33 @@ export class CreateCaseDto { @IsArray() @ArrayMinSize(1) @IsString({ each: true }) + @MaxLength(255, { each: true }) @ApiProperty({ type: String, isArray: true }) readonly policeCaseNumbers!: string[] @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderName?: string @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderEmail?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderPhoneNumber?: string @@ -69,6 +82,7 @@ export class CreateCaseDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly leadInvestigator?: string diff --git a/apps/judicial-system/backend/src/app/modules/case/dto/internalCases.dto.ts b/apps/judicial-system/backend/src/app/modules/case/dto/internalCases.dto.ts index c4b9adffdcd7..d9c3d5c34a08 100644 --- a/apps/judicial-system/backend/src/app/modules/case/dto/internalCases.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/case/dto/internalCases.dto.ts @@ -1,10 +1,16 @@ -import { IsNotEmpty, IsString } from 'class-validator' +import { Transform } from 'class-transformer' +import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator' import { ApiProperty } from '@nestjs/swagger' +import { nationalIdTransformer } from '../../../transformers' + export class InternalCasesDto { @IsNotEmpty() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiProperty({ type: String }) readonly nationalId!: string } diff --git a/apps/judicial-system/backend/src/app/modules/case/dto/internalCreateCase.dto.ts b/apps/judicial-system/backend/src/app/modules/case/dto/internalCreateCase.dto.ts index 4236ebd30e70..b9a4fc01e903 100644 --- a/apps/judicial-system/backend/src/app/modules/case/dto/internalCreateCase.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/case/dto/internalCreateCase.dto.ts @@ -1,3 +1,4 @@ +import { Transform } from 'class-transformer' import { ArrayMinSize, IsArray, @@ -6,12 +7,16 @@ import { IsNotEmpty, IsOptional, IsString, + MaxLength, + MinLength, } from 'class-validator' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { CaseType, Gender } from '@island.is/judicial-system/types' +import { nationalIdTransformer } from '../../../transformers' + export class InternalCreateCaseDto { @IsNotEmpty() @IsEnum(CaseType) @@ -27,11 +32,17 @@ export class InternalCreateCaseDto { @IsNotEmpty() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiProperty({ type: String }) readonly prosecutorNationalId!: string @IsNotEmpty() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiProperty({ type: String }) readonly accusedNationalId!: string diff --git a/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts b/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts index 4930b0b976bb..b4df9da3328d 100644 --- a/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts @@ -1,4 +1,4 @@ -import { Type } from 'class-transformer' +import { Transform, Type } from 'class-transformer' import { ArrayMinSize, IsArray, @@ -9,6 +9,8 @@ import { IsOptional, IsString, IsUUID, + MaxLength, + MinLength, ValidateNested, } from 'class-validator' @@ -35,6 +37,8 @@ import { UserRole, } from '@island.is/judicial-system/types' +import { nationalIdTransformer } from '../../../transformers' + class UpdateDateLog { @IsOptional() @Type(() => Date) @@ -44,6 +48,7 @@ class UpdateDateLog { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly location?: string } @@ -61,6 +66,7 @@ export class UpdateCaseDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly description?: string @@ -68,26 +74,33 @@ export class UpdateCaseDto { @IsArray() @ArrayMinSize(1) @IsString({ each: true }) + @MaxLength(255, { each: true }) @ApiPropertyOptional({ type: String, isArray: true }) readonly policeCaseNumbers?: string[] @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderName?: string @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderEmail?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderPhoneNumber?: string @@ -108,6 +121,7 @@ export class UpdateCaseDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly leadInvestigator?: string @@ -125,6 +139,7 @@ export class UpdateCaseDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly translator?: string @@ -208,6 +223,7 @@ export class UpdateCaseDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly courtCaseNumber?: string @@ -230,6 +246,7 @@ export class UpdateCaseDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly courtLocation?: string @@ -408,6 +425,7 @@ export class UpdateCaseDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly appealCaseNumber?: string diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts index e38b445c792b..36dbfdc8671c 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts @@ -6,7 +6,6 @@ import { CaseState, CaseType, EventType, - getIndictmentVerdictAppealDeadlineStatus, IndictmentCaseReviewDecision, isCourtOfAppealsUser, isDefenceUser, @@ -278,7 +277,9 @@ const canPrisonAdminUserAccessCase = ( // Check case indictment ruling decision access if ( - theCase.indictmentRulingDecision !== CaseIndictmentRulingDecision.RULING + theCase.indictmentRulingDecision !== + CaseIndictmentRulingDecision.RULING && + theCase.indictmentRulingDecision !== CaseIndictmentRulingDecision.FINE ) { return false } @@ -290,16 +291,10 @@ const canPrisonAdminUserAccessCase = ( return false } - // Check defendant verdict appeal deadline access - const canAppealVerdict = true - const verdictInfo = (theCase.defendants || []).map< - [boolean, Date | undefined] - >((defendant) => [canAppealVerdict, defendant.verdictViewDate]) - - const [_, indictmentVerdictAppealDeadlineExpired] = - getIndictmentVerdictAppealDeadlineStatus(verdictInfo) - - if (!indictmentVerdictAppealDeadlineExpired) { + // Check if a defendant has been sent to the prison admin + if ( + !theCase.defendants?.some((defendant) => defendant.isSentToPrisonAdmin) + ) { return false } } 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 e4bf7c5af74d..bc5f2f9f3a5f 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 @@ -25,7 +25,6 @@ import { RequestSharedWithDefender, restrictionCases, UserRole, - VERDICT_APPEAL_WINDOW_DAYS, } from '@island.is/judicial-system/types' const getProsecutionUserCasesQueryFilter = (user: User): WhereOptions => { @@ -212,15 +211,18 @@ const getPrisonAdminUserCasesQueryFilter = (): WhereOptions => { { type: indictmentCases, state: CaseState.COMPLETED, - indictment_ruling_decision: CaseIndictmentRulingDecision.RULING, + indictment_ruling_decision: { + [Op.or]: [ + CaseIndictmentRulingDecision.RULING, + CaseIndictmentRulingDecision.FINE, + ], + }, indictment_review_decision: IndictmentCaseReviewDecision.ACCEPT, id: { - [Op.notIn]: Sequelize.literal(` + [Op.in]: Sequelize.literal(` (SELECT case_id FROM defendant - WHERE (verdict_appeal_date IS NOT NULL - OR verdict_view_date IS NULL - OR verdict_view_date > NOW() - INTERVAL '${VERDICT_APPEAL_WINDOW_DAYS} days')) + WHERE is_sent_to_prison_admin = true) `), }, }, diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts index eb3cc40db316..4d6027e5d5b4 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts @@ -21,7 +21,6 @@ import { RequestSharedWithDefender, restrictionCases, UserRole, - VERDICT_APPEAL_WINDOW_DAYS, } from '@island.is/judicial-system/types' import { getCasesQueryFilter } from '../cases.filter' @@ -389,15 +388,18 @@ describe('getCasesQueryFilter', () => { { type: indictmentCases, state: CaseState.COMPLETED, - indictment_ruling_decision: CaseIndictmentRulingDecision.RULING, + indictment_ruling_decision: { + [Op.or]: [ + CaseIndictmentRulingDecision.RULING, + CaseIndictmentRulingDecision.FINE, + ], + }, indictment_review_decision: IndictmentCaseReviewDecision.ACCEPT, id: { - [Op.notIn]: Sequelize.literal(` + [Op.in]: Sequelize.literal(` (SELECT case_id FROM defendant - WHERE (verdict_appeal_date IS NOT NULL - OR verdict_view_date IS NULL - OR verdict_view_date > NOW() - INTERVAL '${VERDICT_APPEAL_WINDOW_DAYS} days')) + WHERE is_sent_to_prison_admin = true) `), }, }, diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/prisonAdminUserFilter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/prisonAdminUserFilter.spec.ts index de0e36f943a2..9d147a4ca169 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/test/prisonAdminUserFilter.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/prisonAdminUserFilter.spec.ts @@ -132,6 +132,7 @@ describe.each(prisonSystemRoles)('prison admin user %s', (role) => { (state) => { const accessibleCaseIndictmentRulingDecisions = [ CaseIndictmentRulingDecision.RULING, + CaseIndictmentRulingDecision.FINE, ] describe.each( @@ -191,14 +192,29 @@ describe.each(prisonSystemRoles)('prison admin user %s', (role) => { describe.each(accessibleIndictmentCaseReviewDecisions)( 'accessible indictment case review decision %s', (indictmentReviewDecision) => { - const theCase = { - type, - state, - indictmentRulingDecision, - indictmentReviewDecision, - } as Case + describe('no defendant has been sent to the prison admin', () => { + const theCase = { + type, + state, + indictmentRulingDecision, + indictmentReviewDecision, + defendants: [{}], + } as Case + + verifyNoAccess(theCase, user) + }) + + describe('a defendant has been sent to the prison admin', () => { + const theCase = { + type, + state, + indictmentRulingDecision, + indictmentReviewDecision, + defendants: [{ isSentToPrisonAdmin: true }], + } as Case - verifyReadAccess(theCase, user) + verifyReadAccess(theCase, user) + }) }, ) }, diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts index c22e624b71a1..85e302ad6754 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts @@ -7,12 +7,23 @@ import { NestInterceptor, } from '@nestjs/common' +import { Defendant, DefendantEventLog } from '../../defendant' import { Case } from '../models/case.model' import { CaseString } from '../models/caseString.model' +export const transformDefendants = (defendants?: Defendant[]) => { + return defendants?.map((defendant) => ({ + ...defendant.toJSON(), + sentToPrisonAdminDate: defendant.isSentToPrisonAdmin + ? DefendantEventLog.sentToPrisonAdminDate(defendant.eventLogs)?.created + : undefined, + })) +} + const transformCase = (theCase: Case) => { return { ...theCase.toJSON(), + defendants: transformDefendants(theCase.defendants), postponedIndefinitelyExplanation: CaseString.postponedIndefinitelyExplanation(theCase.caseStrings), civilDemands: CaseString.civilDemands(theCase.caseStrings), 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 3044fdff06de..dbc56797bcb1 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 @@ -12,6 +12,7 @@ import { IndictmentDecision } from '@island.is/judicial-system/types' import { Case } from '../models/case.model' import { CaseString } from '../models/caseString.model' import { DateLog } from '../models/dateLog.model' +import { transformDefendants } from './case.interceptor' @Injectable() export class CaseListInterceptor implements NestInterceptor { @@ -29,7 +30,7 @@ export class CaseListInterceptor implements NestInterceptor { policeCaseNumbers: theCase.policeCaseNumbers, state: theCase.state, type: theCase.type, - defendants: theCase.defendants, + defendants: transformDefendants(theCase.defendants), courtCaseNumber: theCase.courtCaseNumber, decision: theCase.decision, validToDate: theCase.validToDate, 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 2b03c85cdb96..0a7dce8d89b7 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 @@ -178,31 +178,6 @@ export class InternalCaseController { ) } - @UseGuards(CaseExistsGuard, new CaseTypeGuard(indictmentCases)) - @Post( - `case/:caseId/${ - messageEndpoint[MessageType.DELIVERY_TO_COURT_INDICTMENT_DEFENDER] - }`, - ) - @ApiOkResponse({ - type: DeliverResponse, - description: 'Delivers indictment case defender info to court', - }) - deliverIndictmentDefenderInfoToCourt( - @Param('caseId') caseId: string, - @CurrentCase() theCase: Case, - @Body() deliverDto: DeliverDto, - ): Promise { - this.logger.debug( - `Delivering the indictment defender info for case ${caseId} to court`, - ) - - return this.internalCaseService.deliverIndictmentDefenderInfoToCourt( - 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 211931752bfc..c47fd37ca093 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 @@ -418,7 +418,7 @@ export class InternalCaseService { collectEncryptionProperties(defendantEncryptionProperties, defendant) defendantsArchive.push(defendantArchive) - await this.defendantService.updateForArcive( + await this.defendantService.updateDatabaseDefendant( theCase.id, defendant.id, clearedDefendantProperties, @@ -580,6 +580,10 @@ export class InternalCaseService { : [] const mappedSubtypes = subtypeList.flatMap((key) => courtSubtypes[key]) + const indictmentIssuedByProsecutorAndReceivedByCourt = + theCase.eventLogs?.find( + (eventLog) => eventLog.eventType === EventType.INDICTMENT_CONFIRMED, + )?.created return this.courtService .updateIndictmentCaseWithIndictmentInfo( @@ -587,12 +591,8 @@ export class InternalCaseService { theCase.id, theCase.court?.name, theCase.courtCaseNumber, - theCase.eventLogs?.find( - (eventLog) => eventLog.eventType === EventType.CASE_RECEIVED_BY_COURT, - )?.created, - theCase.eventLogs?.find( - (eventLog) => eventLog.eventType === EventType.INDICTMENT_CONFIRMED, - )?.created, + indictmentIssuedByProsecutorAndReceivedByCourt, + indictmentIssuedByProsecutorAndReceivedByCourt, theCase.policeCaseNumbers[0], mappedSubtypes, theCase.defendants?.map((defendant) => ({ @@ -617,29 +617,6 @@ export class InternalCaseService { }) } - async deliverIndictmentDefenderInfoToCourt( - theCase: Case, - user: TUser, - ): Promise { - return this.courtService - .updateIndictmentCaseWithDefenderInfo( - user, - theCase.id, - theCase.court?.name, - theCase.courtCaseNumber, - theCase.defendants, - ) - .then(() => ({ delivered: true })) - .catch((reason) => { - this.logger.error( - `Failed to update indictment case ${theCase.id} with defender info`, - { reason }, - ) - - return { delivered: false } - }) - } - async deliverIndictmentAssignedRolesToCourt( theCase: Case, user: TUser, 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 bd17a87781d9..14e595b23ff0 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 @@ -23,6 +23,7 @@ import { CaseNotificationType, CaseState, dateTypes, + defendantEventTypes, eventTypes, stringTypes, UserRole, @@ -34,6 +35,7 @@ import { CivilClaimant, CivilClaimantService, Defendant, + DefendantEventLog, DefendantService, } from '../defendant' import { EventLog } from '../event-log' @@ -182,6 +184,14 @@ export const include: Includeable[] = [ order: [['created', 'DESC']], separate: true, }, + { + model: DefendantEventLog, + as: 'eventLogs', + required: false, + where: { eventType: defendantEventTypes }, + order: [['created', 'DESC']], + separate: true, + }, ], separate: true, }, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/createCourtCase.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/createCourtCase.spec.ts index 318a3420af0d..b1daaac68f15 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/createCourtCase.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/createCourtCase.spec.ts @@ -7,20 +7,23 @@ import { CaseFileState, CaseState, CaseType, + EventType, IndictmentSubtype, investigationCases, - isIndictmentCase, restrictionCases, User as TUser, } from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' -import { randomEnum } from '../../../../test' +import { nowFactory } from '../../../../factories' +import { randomDate, randomEnum } from '../../../../test' import { CourtService } from '../../../court' import { include } from '../../case.service' import { Case } from '../../models/case.model' +jest.mock('../../../../factories') + interface Then { result: Case error: Error @@ -84,16 +87,71 @@ describe('CaseController - Create court case', () => { } }) - describe('court case created', () => { + describe('request court case created', () => { + const date = randomDate() const caseId = uuid() - const type = randomEnum(CaseType) + const type = CaseType.CUSTODY const policeCaseNumber = uuid() - const indictmentSubtype = isIndictmentCase(type) - ? randomEnum(IndictmentSubtype) - : undefined - const indictmentSubtypes = isIndictmentCase(type) - ? { [policeCaseNumber]: [indictmentSubtype] } - : undefined + const policeCaseNumbers = [policeCaseNumber] + const courtId = uuid() + const theCase = { + id: caseId, + type, + policeCaseNumbers, + courtId, + } as Case + const returnedCase = { + id: caseId, + type, + policeCaseNumbers, + courtId, + courtCaseNumber, + } as Case + let then: Then + + beforeEach(async () => { + const mockFindOne = mockCaseModel.findOne as jest.Mock + mockFindOne.mockResolvedValueOnce(returnedCase) + + const mockToday = nowFactory as jest.Mock + mockToday.mockReturnValueOnce(date) + + then = await givenWhenThen(caseId, user, theCase) + }) + + it('should create a court case', () => { + expect(mockCourtService.createCourtCase).toHaveBeenCalledWith( + user, + caseId, + courtId, + type, + date, + policeCaseNumbers, + false, + undefined, + ) + expect(mockCaseModel.update).toHaveBeenCalledWith( + { courtCaseNumber }, + { where: { id: caseId }, transaction }, + ) + expect(mockCaseModel.findOne).toHaveBeenCalledWith({ + include, + where: { + id: caseId, + isArchived: false, + }, + }) + expect(then.result).toBe(returnedCase) + }) + }) + + describe('indictment court case created', () => { + const caseId = uuid() + const type = CaseType.INDICTMENT + const policeCaseNumber = uuid() + const indictmentSubtype = randomEnum(IndictmentSubtype) + const indictmentSubtypes = { [policeCaseNumber]: [indictmentSubtype] } + const indictmentConfirmedDate = randomDate() const policeCaseNumbers = [policeCaseNumber] const courtId = uuid() const theCase = { @@ -102,9 +160,16 @@ describe('CaseController - Create court case', () => { policeCaseNumbers, indictmentSubtypes, courtId, + eventLogs: [ + { + eventType: EventType.INDICTMENT_CONFIRMED, + created: indictmentConfirmedDate, + }, + ], } as Case const returnedCase = { id: caseId, + type, policeCaseNumbers, indictmentSubtypes, courtId, @@ -125,6 +190,7 @@ describe('CaseController - Create court case', () => { caseId, courtId, type, + indictmentConfirmedDate, policeCaseNumbers, false, indictmentSubtypes, @@ -283,11 +349,6 @@ describe('CaseController - Create court case', () => { it('should post to queue', () => { expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ - { - type: MessageType.DELIVERY_TO_COURT_PROSECUTOR, - user, - caseId: theCase.id, - }, { type: MessageType.DELIVERY_TO_COURT_CASE_FILES_RECORD, user, 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 088190ba98dd..b3b0bb816313 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 @@ -458,23 +458,6 @@ describe('CaseController - Update', () => { it('should post to queue', () => { expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ - { - type: MessageType.DELIVERY_TO_COURT_PROSECUTOR, - user, - caseId, - }, - { - type: MessageType.DELIVERY_TO_COURT_DEFENDANT, - user, - caseId, - elementId: defendantId1, - }, - { - type: MessageType.DELIVERY_TO_COURT_DEFENDANT, - user, - caseId, - elementId: defendantId2, - }, { type: MessageType.DELIVERY_TO_COURT_CASE_FILES_RECORD, user, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archive.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archive.spec.ts index d6beb070a348..7f06a822d4aa 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archive.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archive.spec.ts @@ -287,13 +287,13 @@ describe('InternalCaseController - Archive', () => { ], where: archiveFilter, }) - expect(mockDefendantService.updateForArcive).toHaveBeenCalledWith( + expect(mockDefendantService.updateDatabaseDefendant).toHaveBeenCalledWith( caseId, defendantId1, { nationalId: '', name: '', address: '' }, transaction, ) - expect(mockDefendantService.updateForArcive).toHaveBeenCalledWith( + expect(mockDefendantService.updateDatabaseDefendant).toHaveBeenCalledWith( caseId, defendantId2, { nationalId: '', name: '', address: '' }, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentDefenderInfoToCourtGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentDefenderInfoToCourtGuards.spec.ts deleted file mode 100644 index 4fa794094133..000000000000 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentDefenderInfoToCourtGuards.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { CanActivate } from '@nestjs/common' - -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 defender info to court guards', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let guards: any[] - - beforeEach(() => { - guards = Reflect.getMetadata( - '__guards__', - InternalCaseController.prototype.deliverIndictmentDefenderInfoToCourt, - ) - }) - - it('should have two guards', () => { - 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, - }) - }) - }) -}) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts index 4c929c759e7a..3236f05eb4de 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts @@ -96,7 +96,7 @@ describe('InternalCaseController - Deliver indictment info to court', () => { caseId, courtName, courtCaseNumber, - receivedDate, + indictmentDate, indictmentDate, policeCaseNumber, ['Umferðarlagabrot', 'Hylming', 'Þjófnaður'], diff --git a/apps/judicial-system/backend/src/app/modules/court/court.service.ts b/apps/judicial-system/backend/src/app/modules/court/court.service.ts index 3ff75964c054..5a521901c9a6 100644 --- a/apps/judicial-system/backend/src/app/modules/court/court.service.ts +++ b/apps/judicial-system/backend/src/app/modules/court/court.service.ts @@ -24,8 +24,6 @@ import { isIndictmentCase, } from '@island.is/judicial-system/types' -import { nowFactory } from '../../factories' -import { Defendant } from '../defendant' import { EventService } from '../event' import { RobotLog } from './models/robotLog.model' import { courtModuleConfig } from './court.config' @@ -324,6 +322,7 @@ export class CourtService { caseId: string, courtId = '', type: CaseType, + receivalDate: Date, policeCaseNumbers: string[], isExtension: boolean, indictmentSubtypes?: IndictmentSubtypeMap, @@ -342,7 +341,7 @@ export class CourtService { caseType: isIndictment ? 'S - Ákærumál' : 'R - Rannsóknarmál', subtype: courtSubtype as string, status: 'Skráð', - receivalDate: formatISO(nowFactory(), { representation: 'date' }), + receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: isIndictment ? 'Sakamál' : 'Rannsóknarhagsmunir', // TODO: pass in all policeCaseNumbers when CourtService supports it sourceNumber: policeCaseNumbers[0] ? policeCaseNumbers[0] : '', @@ -615,17 +614,17 @@ export class CourtService { caseId: string, courtName?: string, courtCaseNumber?: string, - defendants?: Defendant[], + defendantNationalId?: string, + defenderName?: string, + defenderEmail?: string, ): Promise { try { - const defendantInfo = defendants?.map((defendant) => ({ - nationalId: defendant.nationalId, - defenderName: defendant.defenderName, - defenderEmail: defendant.defenderEmail, - })) - - const subject = `${courtName} - ${courtCaseNumber} - verjanda upplýsingar` - const content = JSON.stringify({ defendants: defendantInfo, courtName }) + const subject = `${courtName} - ${courtCaseNumber} - verjandi varnaraðila` + const content = JSON.stringify({ + nationalId: defendantNationalId, + defenderName, + defenderEmail, + }) return this.sendToRobot( subject, @@ -635,7 +634,7 @@ export class CourtService { ) } catch (error) { this.eventService.postErrorEvent( - 'Failed to update indictment with defender info', + 'Failed to update indictment case with defender info', { caseId, actor: user.name, diff --git a/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts b/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts index d1be752555d1..a32798d80b4e 100644 --- a/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts @@ -14,12 +14,9 @@ import { import { createTestingCourtModule } from './createTestingCourtModule' -import { nowFactory } from '../../../factories' import { randomBoolean, randomDate, randomEnum } from '../../../test' import { courtSubtypes, Subtype } from '../court.service' -jest.mock('../../../factories') - interface Then { result: string error: Error @@ -30,13 +27,14 @@ type GivenWhenThen = ( caseId: string, courtId: string, type: CaseType, + receivalDate: Date, policeCaseNumbers: string[], isExtension: boolean, indictmentSubtypes?: IndictmentSubtypeMap, ) => Promise describe('CourtService - Create court case', () => { - const date = randomDate() + const receivalDate = randomDate() let mockCourtClientService: CourtClientService let givenWhenThen: GivenWhenThen @@ -46,14 +44,12 @@ describe('CourtService - Create court case', () => { mockCourtClientService = courtClientService - const mockToday = nowFactory as jest.Mock - mockToday.mockReturnValueOnce(date) - givenWhenThen = async ( user: User, caseId: string, courtId: string, type: CaseType, + receivalDate: Date, policeCaseNumbers: string[], isExtension: boolean, indictmentSubtypes?: IndictmentSubtypeMap, @@ -66,6 +62,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, indictmentSubtypes, @@ -93,6 +90,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, ) @@ -105,7 +103,7 @@ describe('CourtService - Create court case', () => { caseType: 'R - Rannsóknarmál', subtype: courtSubtypes[type as Subtype], status: 'Skráð', - receivalDate: formatISO(date, { representation: 'date' }), + receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Rannsóknarhagsmunir', sourceNumber: policeCaseNumbers[0], }, @@ -132,6 +130,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, indictmentSubtypes, @@ -145,7 +144,7 @@ describe('CourtService - Create court case', () => { caseType: 'S - Ákærumál', subtype: courtSubtypes[indictmentSubtype], status: 'Skráð', - receivalDate: formatISO(date, { representation: 'date' }), + receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Sakamál', sourceNumber: policeCaseNumbers[0], }, @@ -171,6 +170,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, ) @@ -181,7 +181,7 @@ describe('CourtService - Create court case', () => { caseType: 'R - Rannsóknarmál', subtype: courtSubtypes[type as Subtype][0], status: 'Skráð', - receivalDate: formatISO(date, { representation: 'date' }), + receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Rannsóknarhagsmunir', sourceNumber: policeCaseNumbers[0], }) @@ -205,6 +205,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, ) @@ -215,7 +216,7 @@ describe('CourtService - Create court case', () => { caseType: 'R - Rannsóknarmál', subtype: courtSubtypes[type as Subtype][1], status: 'Skráð', - receivalDate: formatISO(date, { representation: 'date' }), + receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Rannsóknarhagsmunir', sourceNumber: policeCaseNumbers[0], }) @@ -248,6 +249,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, indictmentSubtypes, @@ -284,6 +286,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, indictmentSubtypes, diff --git a/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts b/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts index 950e62bf84da..285092718ba5 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts @@ -7,7 +7,11 @@ import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' -import { CaseState } from '@island.is/judicial-system/types' +import { MessageService, MessageType } from '@island.is/judicial-system/message' +import { + CaseState, + CivilClaimantNotificationType, +} from '@island.is/judicial-system/types' import { Case } from '../case/models/case.model' import { UpdateCivilClaimantDto } from './dto/updateCivilClaimant.dto' @@ -18,6 +22,7 @@ export class CivilClaimantService { constructor( @InjectModel(CivilClaimant) private readonly civilClaimantModel: typeof CivilClaimant, + private readonly messageService: MessageService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -27,6 +32,24 @@ export class CivilClaimantService { }) } + private async sendUpdateCivilClaimantMessages( + update: UpdateCivilClaimantDto, + updatedCivilClaimant: CivilClaimant, + ): Promise { + if (update.isSpokespersonConfirmed === true) { + return this.messageService.sendMessagesToQueue([ + { + type: MessageType.CIVIL_CLAIMANT_NOTIFICATION, + caseId: updatedCivilClaimant.caseId, + body: { + type: CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + }, + elementId: updatedCivilClaimant.id, + }, + ]) + } + } + async update( caseId: string, civilClaimantId: string, @@ -49,6 +72,8 @@ export class CivilClaimantService { throw new Error(`Could not update civil claimant ${civilClaimantId}`) } + await this.sendUpdateCivilClaimantMessages(update, civilClaimants[0]) + return civilClaimants[0] } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts b/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts index 64517bda3c3a..3aa77ff57b8f 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts @@ -7,6 +7,7 @@ import { CaseModule } from '../case/case.module' import { CourtModule } from '../court/court.module' import { CivilClaimant } from './models/civilClaimant.model' import { Defendant } from './models/defendant.model' +import { DefendantEventLog } from './models/defendantEventLog.model' import { CivilClaimantController } from './civilClaimant.controller' import { CivilClaimantService } from './civilClaimant.service' import { DefendantController } from './defendant.controller' @@ -18,7 +19,7 @@ import { InternalDefendantController } from './internalDefendant.controller' MessageModule, forwardRef(() => CourtModule), forwardRef(() => CaseModule), - SequelizeModule.forFeature([Defendant, CivilClaimant]), + SequelizeModule.forFeature([Defendant, CivilClaimant, DefendantEventLog]), ], controllers: [ DefendantController, 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 38374e35a08b..59ca01645a40 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 @@ -22,6 +22,7 @@ import { CaseNotificationType, CaseState, CaseType, + DefendantEventType, DefendantNotificationType, DefenderChoice, isIndictmentCase, @@ -33,12 +34,15 @@ import { CreateDefendantDto } from './dto/createDefendant.dto' import { InternalUpdateDefendantDto } from './dto/internalUpdateDefendant.dto' import { UpdateDefendantDto } from './dto/updateDefendant.dto' import { Defendant } from './models/defendant.model' +import { DefendantEventLog } from './models/defendantEventLog.model' import { DeliverResponse } from './models/deliver.response' @Injectable() export class DefendantService { constructor( @InjectModel(Defendant) private readonly defendantModel: typeof Defendant, + @InjectModel(DefendantEventLog) + private readonly defendantEventLogModel: typeof DefendantEventLog, private readonly courtService: CourtService, private readonly messageService: MessageService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, @@ -70,6 +74,26 @@ export class DefendantService { return message } + private getMessagesForIndictmentToPrisonAdminChanges( + defendant: Defendant, + ): Message { + const messageType = + defendant.isSentToPrisonAdmin === true + ? DefendantNotificationType.INDICTMENT_SENT_TO_PRISON_ADMIN + : DefendantNotificationType.INDICTMENT_WITHDRAWN_FROM_PRISON_ADMIN + + const message = { + type: MessageType.DEFENDANT_NOTIFICATION, + caseId: defendant.caseId, + elementId: defendant.id, + body: { + type: messageType, + }, + } + + return message + } + private getUpdatedDefendant( numberOfAffectedRows: number, defendants: Defendant[], @@ -90,64 +114,97 @@ export class DefendantService { return defendants[0] } - private async sendUpdateDefendantMessages( + private async sendRequestCaseUpdateDefendantMessages( theCase: Case, updatedDefendant: Defendant, oldDefendant: Defendant, user: User, - ) { + ): Promise { + if (!theCase.courtCaseNumber) { + return + } + + const messages: Message[] = [] + // Handling of updates sent to the court system - if (theCase.courtCaseNumber) { - // A defendant is updated after the case has been received by the court. - if (updatedDefendant.noNationalId !== oldDefendant.noNationalId) { - // This should only happen to non-indictment cases. - // A defendant nationalId is added or removed. Attempt to add the defendant to the court case. - // In case there is no national id, the court will be notified. - await this.messageService.sendMessagesToQueue([ - this.getMessageForDeliverDefendantToCourt(updatedDefendant, user), - ]) - } else if (updatedDefendant.nationalId !== oldDefendant.nationalId) { - // This should only happen to non-indictment cases. - // A defendant is replaced. Attempt to add the defendant to the court case, - // but also ask the court to verify defendants. - await this.messageService.sendMessagesToQueue([ - this.getMessageForSendDefendantsNotUpdatedAtCourtNotification( - theCase, - user, - ), - this.getMessageForDeliverDefendantToCourt(updatedDefendant, user), - ]) - } else if ( - updatedDefendant.defenderEmail !== oldDefendant.defenderEmail - ) { - // This should only happen to indictment cases. - // A defendant's defender email is updated. - // Attempt to update the defendant in the court case. - await this.messageService.sendMessagesToQueue([ - this.getMessageForDeliverDefendantToCourt(updatedDefendant, user), - ]) - } + // A defendant is updated after the case has been received by the court. + if (updatedDefendant.noNationalId !== oldDefendant.noNationalId) { + // A defendant nationalId is added or removed. Attempt to add the defendant to the court case. + // In case there is no national id, the court will be notified. + messages.push( + this.getMessageForDeliverDefendantToCourt(updatedDefendant, user), + ) + } else if (updatedDefendant.nationalId !== oldDefendant.nationalId) { + // A defendant is replaced. Attempt to add the defendant to the court case, + // but also ask the court to verify defendants. + messages.push( + this.getMessageForSendDefendantsNotUpdatedAtCourtNotification( + theCase, + user, + ), + this.getMessageForDeliverDefendantToCourt(updatedDefendant, user), + ) } - // Handling of messages sent to participants for indictment cases - if (isIndictmentCase(theCase.type)) { - // Defender was just confirmed by judge + if (messages.length === 0) { + return + } + + return this.messageService.sendMessagesToQueue(messages) + } + + private async sendIndictmentCaseUpdateDefendantMessages( + theCase: Case, + updatedDefendant: Defendant, + oldDefendant: Defendant, + user: User, + ): Promise { + if (!theCase.courtCaseNumber) { + return + } + + const messages: Message[] = [] + + if ( + updatedDefendant.isDefenderChoiceConfirmed && + !oldDefendant.isDefenderChoiceConfirmed + ) { + // Defender choice was just confirmed by the court + messages.push({ + type: MessageType.DELIVERY_TO_COURT_INDICTMENT_DEFENDER, + user, + caseId: theCase.id, + elementId: updatedDefendant.id, + }) + if ( - updatedDefendant.isDefenderChoiceConfirmed && - !oldDefendant.isDefenderChoiceConfirmed && - (updatedDefendant.defenderChoice === DefenderChoice.CHOOSE || - updatedDefendant.defenderChoice === DefenderChoice.DELEGATE) + updatedDefendant.defenderChoice === DefenderChoice.CHOOSE || + updatedDefendant.defenderChoice === DefenderChoice.DELEGATE ) { - await this.messageService.sendMessagesToQueue([ - { + // Defender was just confirmed by judge + if (!oldDefendant.isDefenderChoiceConfirmed) { + messages.push({ type: MessageType.DEFENDANT_NOTIFICATION, caseId: theCase.id, body: { type: DefendantNotificationType.DEFENDER_ASSIGNED }, elementId: updatedDefendant.id, - }, - ]) + }) + } } + } else if ( + updatedDefendant.isSentToPrisonAdmin !== undefined && + updatedDefendant.isSentToPrisonAdmin !== oldDefendant.isSentToPrisonAdmin + ) { + messages.push( + this.getMessagesForIndictmentToPrisonAdminChanges(updatedDefendant), + ) + } + + if (messages.length === 0) { + return } + + return this.messageService.sendMessagesToQueue(messages) } async createForNewCase( @@ -183,19 +240,15 @@ export class DefendantService { return defendant } - async updateForArcive( + async updateDatabaseDefendant( caseId: string, defendantId: string, update: UpdateDefendantDto, - transaction: Transaction, - ): Promise { + transaction?: Transaction, + ) { const [numberOfAffectedRows, defendants] = await this.defendantModel.update( update, - { - where: { id: defendantId, caseId }, - returning: true, - transaction, - }, + { where: { id: defendantId, caseId }, returning: true, transaction }, ) return this.getUpdatedDefendant( @@ -206,28 +259,49 @@ export class DefendantService { ) } - async update( + async updateRequestCaseDefendant( theCase: Case, defendant: Defendant, update: UpdateDefendantDto, user: User, ): Promise { - const [numberOfAffectedRows, defendants] = await this.defendantModel.update( + const updatedDefendant = await this.updateDatabaseDefendant( + theCase.id, + defendant.id, update, - { - where: { id: defendant.id, caseId: theCase.id }, - returning: true, - }, ) - const updatedDefendant = this.getUpdatedDefendant( - numberOfAffectedRows, - defendants, - defendant.id, + await this.sendRequestCaseUpdateDefendantMessages( + theCase, + updatedDefendant, + defendant, + user, + ) + + return updatedDefendant + } + + async updateIndictmentCaseDefendant( + theCase: Case, + defendant: Defendant, + update: UpdateDefendantDto, + user: User, + ): Promise { + const updatedDefendant = await this.updateDatabaseDefendant( theCase.id, + defendant.id, + update, ) - await this.sendUpdateDefendantMessages( + if (update.isSentToPrisonAdmin) { + this.defendantEventLogModel.create({ + caseId: theCase.id, + defendantId: defendant.id, + eventType: DefendantEventType.SENT_TO_PRISON_ADMIN, + }) + } + + await this.sendIndictmentCaseUpdateDefendantMessages( theCase, updatedDefendant, defendant, @@ -237,38 +311,62 @@ export class DefendantService { return updatedDefendant } - async updateByNationalId( - caseId: string, - defendantNationalId: string, + async update( + theCase: Case, + defendant: Defendant, + update: UpdateDefendantDto, + user: User, + ): Promise { + if (isIndictmentCase(theCase.type)) { + return this.updateIndictmentCaseDefendant( + theCase, + defendant, + update, + user, + ) + } else { + return this.updateRequestCaseDefendant(theCase, defendant, update, user) + } + } + + async updateRestricted( + theCase: Case, + defendant: Defendant, update: InternalUpdateDefendantDto, + isDefenderChoiceConfirmed = false, + transaction?: Transaction, ): Promise { // The reason we have a separate dto for this is because requests that end here // are initiated by outside API's which should not be able to edit other fields // Defendant updated originating from the judicial system should use the UpdateDefendantDto // and go through the update method above using the defendantId. - // This is also why we set the isDefenderChoiceConfirmed to false here - the judge needs to confirm all changes. - update = { - ...update, - isDefenderChoiceConfirmed: false, - } as UpdateDefendantDto + // This is also why we may set the isDefenderChoiceConfirmed to false here - the judge needs to confirm all changes. - const [numberOfAffectedRows, defendants] = await this.defendantModel.update( - update, - { - where: { - caseId, - national_id: normalizeAndFormatNationalId(defendantNationalId), - }, - returning: true, - }, + const updatedDefendant = await this.updateDatabaseDefendant( + theCase.id, + defendant.id, + { ...update, isDefenderChoiceConfirmed }, + transaction, ) - const updatedDefendant = this.getUpdatedDefendant( - numberOfAffectedRows, - defendants, - defendants[0].id, - caseId, - ) + // Notify the court if the defendant has changed the defender choice + if ( + !updatedDefendant.isDefenderChoiceConfirmed && + updatedDefendant.defenderChoice === DefenderChoice.CHOOSE && + (updatedDefendant.defenderChoice !== defendant.defenderChoice || + updatedDefendant.defenderNationalId !== defendant.defenderNationalId) + ) { + await this.messageService.sendMessagesToQueue([ + { + type: MessageType.DEFENDANT_NOTIFICATION, + caseId: theCase.id, + elementId: updatedDefendant.id, + body: { + type: DefendantNotificationType.DEFENDANT_SELECTED_DEFENDER, + }, + }, + ]) + } return updatedDefendant } @@ -393,4 +491,30 @@ export class DefendantService { return { delivered: false } }) } + + async deliverIndictmentDefenderToCourt( + theCase: Case, + defendant: Defendant, + user: User, + ): Promise { + return this.courtService + .updateIndictmentCaseWithDefenderInfo( + user, + theCase.id, + theCase.court?.name, + theCase.courtCaseNumber, + defendant.nationalId, + defendant.defenderName, + defendant.defenderEmail, + ) + .then(() => ({ delivered: true })) + .catch((reason) => { + this.logger.error( + `Failed to update defender info for defendant ${defendant.id} of indictment case ${theCase.id}`, + { reason }, + ) + + return { delivered: false } + }) + } } 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 eee5ea58da99..d68a4e8f2995 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 @@ -1,9 +1,19 @@ -import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator' +import { Transform } from 'class-transformer' +import { + IsBoolean, + IsEnum, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' import { DefenderChoice, Gender } from '@island.is/judicial-system/types' +import { nationalIdTransformer } from '../../../transformers' + export class CreateDefendantDto { @IsOptional() @IsBoolean() @@ -12,11 +22,15 @@ export class CreateDefendantDto { @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly nationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly name?: string @@ -27,31 +41,39 @@ export class CreateDefendantDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly address?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly citizenship?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderName?: string @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderEmail?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderPhoneNumber?: string diff --git a/apps/judicial-system/backend/src/app/modules/defendant/dto/deliverDefendantToCourt.dto.ts b/apps/judicial-system/backend/src/app/modules/defendant/dto/deliver.dto.ts similarity index 85% rename from apps/judicial-system/backend/src/app/modules/defendant/dto/deliverDefendantToCourt.dto.ts rename to apps/judicial-system/backend/src/app/modules/defendant/dto/deliver.dto.ts index 7671fe935ed8..7595952ea24d 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/dto/deliverDefendantToCourt.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/dto/deliver.dto.ts @@ -4,7 +4,7 @@ import { ApiProperty } from '@nestjs/swagger' import type { User } from '@island.is/judicial-system/types' -export class DeliverDefendantToCourtDto { +export class DeliverDto { @IsNotEmpty() @IsObject() @ApiProperty({ type: Object }) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/dto/internalUpdateDefendant.dto.ts b/apps/judicial-system/backend/src/app/modules/defendant/dto/internalUpdateDefendant.dto.ts index 8cfac3f40ade..54b7c149fda2 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/dto/internalUpdateDefendant.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/dto/internalUpdateDefendant.dto.ts @@ -1,9 +1,18 @@ -import { IsEnum, IsOptional, IsString } from 'class-validator' +import { Transform } from 'class-transformer' +import { + IsEnum, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' import { DefenderChoice } from '@island.is/judicial-system/types' +import { nationalIdTransformer } from '../../../transformers' + export class InternalUpdateDefendantDto { @IsOptional() @IsString() @@ -12,6 +21,9 @@ export class InternalUpdateDefendantDto { @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @@ -37,6 +49,9 @@ export class InternalUpdateDefendantDto { @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly requestedDefenderNationalId?: string diff --git a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateCivilClaimant.dto.ts b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateCivilClaimant.dto.ts index 3f85ec624a7f..67851cf47a0a 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateCivilClaimant.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateCivilClaimant.dto.ts @@ -1,7 +1,16 @@ -import { IsBoolean, IsOptional, IsString } from 'class-validator' +import { Transform } from 'class-transformer' +import { + IsBoolean, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' +import { nationalIdTransformer } from '../../../transformers' + export class UpdateCivilClaimantDto { @IsOptional() @IsBoolean() @@ -10,13 +19,17 @@ export class UpdateCivilClaimantDto { @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) - readonly name?: string + readonly nationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) - readonly nationalId?: string + readonly name?: string @IsOptional() @IsBoolean() @@ -30,21 +43,27 @@ export class UpdateCivilClaimantDto { @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly spokespersonNationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly spokespersonName?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly spokespersonEmail?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly spokespersonPhoneNumber?: string 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 8e9321fbf730..611604fb8c5d 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 @@ -1,10 +1,12 @@ -import { Type } from 'class-transformer' +import { Transform, Type } from 'class-transformer' import { IsBoolean, IsDate, IsEnum, IsOptional, IsString, + MaxLength, + MinLength, } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' @@ -17,6 +19,8 @@ import { SubpoenaType, } from '@island.is/judicial-system/types' +import { nationalIdTransformer } from '../../../transformers' + export class UpdateDefendantDto { @IsOptional() @IsBoolean() @@ -25,11 +29,15 @@ export class UpdateDefendantDto { @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly nationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly name?: string @@ -40,31 +48,39 @@ export class UpdateDefendantDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly address?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly citizenship?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderName?: string @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderEmail?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderPhoneNumber?: string @@ -107,11 +123,15 @@ export class UpdateDefendantDto { @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly requestedDefenderNationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly requestedDefenderName?: string @@ -124,4 +144,9 @@ export class UpdateDefendantDto { @IsBoolean() @ApiPropertyOptional({ type: Boolean }) readonly caseFilesSharedWithDefender?: boolean + + @IsOptional() + @IsBoolean() + @ApiPropertyOptional({ type: Boolean }) + readonly isSentToPrisonAdmin?: boolean } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimaint.decorator.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimaint.decorator.ts new file mode 100644 index 000000000000..9f508c5e60e5 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimaint.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator } from '@nestjs/common' + +import { CivilClaimant } from '../models/civilClaimant.model' + +export const CurrentCivilClaimant = createParamDecorator( + (data, { args: [_1, { req }] }): CivilClaimant => req.civilClaimant, +) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimantExists.guard.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimantExists.guard.ts new file mode 100644 index 000000000000..401886582d41 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimantExists.guard.ts @@ -0,0 +1,42 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common' + +import { Case } from '../../case' + +@Injectable() +export class CivilClaimantExistsGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() + + const theCase: Case = request.case + + if (!theCase) { + throw new BadRequestException('Missing case') + } + + const civilClaimantId = request.params.civilClaimantId + + if (!civilClaimantId) { + throw new BadRequestException('Missing civil claimant id') + } + + const civilClaimant = theCase.civilClaimants?.find( + (civilClaimants) => civilClaimants.id === civilClaimantId, + ) + + if (!civilClaimant) { + throw new NotFoundException( + `Civil claimant ${civilClaimantId} of case ${theCase.id} does not exist`, + ) + } + + request.civilClaimant = civilClaimant + + return true + } +} diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/defendantNationalIdExists.guard.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/defendantNationalIdExists.guard.ts new file mode 100644 index 000000000000..b400bccebd5e --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/defendantNationalIdExists.guard.ts @@ -0,0 +1,49 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common' + +import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' + +import { Case } from '../../case' + +@Injectable() +export class DefendantNationalIdExistsGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() + + const theCase: Case = request.case + + if (!theCase) { + throw new BadRequestException('Missing case') + } + + const defendantNationalId = request.params.defendantNationalId + + if (!defendantNationalId) { + throw new BadRequestException('Missing defendant national id') + } + + const normalizedAndFormatedNationalId = + normalizeAndFormatNationalId(defendantNationalId) + + const defendant = theCase.defendants?.find( + (defendant) => + defendant.nationalId && + normalizedAndFormatedNationalId.includes(defendant.nationalId), + ) + + if (!defendant) { + throw new NotFoundException( + `Defendant with given national id of case ${theCase.id} does not exist`, + ) + } + + request.defendant = defendant + + return true + } +} diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/test/civilClaimantExistsGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/test/civilClaimantExistsGuard.spec.ts new file mode 100644 index 000000000000..4e5cd39db640 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/test/civilClaimantExistsGuard.spec.ts @@ -0,0 +1,117 @@ +import { uuid } from 'uuidv4' + +import { + BadRequestException, + ExecutionContext, + NotFoundException, +} from '@nestjs/common' + +import { CivilClaimantExistsGuard } from '../civilClaimantExists.guard' + +interface Then { + result: boolean + error: Error +} + +type GivenWhenThen = () => Promise + +describe('Civil Claimant Exists Guard', () => { + const mockRequest = jest.fn() + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + givenWhenThen = async (): Promise => { + const guard = new CivilClaimantExistsGuard() + const then = {} as Then + + try { + then.result = guard.canActivate({ + switchToHttp: () => ({ getRequest: mockRequest }), + } as unknown as ExecutionContext) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe('civil claimant exists', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const civilClaimant = { id: civilClaimantId, caseId } + const theCase = { id: caseId, civilClaimants: [civilClaimant] } + const request = { + params: { caseId, civilClaimantId }, + case: theCase, + civilClaimant: undefined, + } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce(request) + + then = await givenWhenThen() + }) + + it('should activate', () => { + expect(then.result).toBe(true) + expect(request.civilClaimant).toBe(civilClaimant) + }) + }) + + describe('civil claimant does not exist', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const theCase = { id: caseId, civilClaimants: [] } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ + params: { caseId, civilClaimantId }, + case: theCase, + }) + + then = await givenWhenThen() + }) + + it('should throw NotFoundException', () => { + expect(then.error).toBeInstanceOf(NotFoundException) + expect(then.error.message).toBe( + `Civil claimant ${civilClaimantId} of case ${caseId} does not exist`, + ) + }) + }) + + describe('missing case', () => { + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ params: {} }) + + then = await givenWhenThen() + }) + + it('should throw BadRequestException', () => { + expect(then.error).toBeInstanceOf(BadRequestException) + expect(then.error.message).toBe('Missing case') + }) + }) + + describe('missing civil claimant id', () => { + const caseId = uuid() + const theCase = { id: caseId, civilClaimants: [] } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ params: { caseId }, case: theCase }) + + then = await givenWhenThen() + }) + + it('should throw BadRequestException', () => { + expect(then.error).toBeInstanceOf(BadRequestException) + expect(then.error.message).toBe('Missing civil claimant id') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/test/defendantNationalIdExistsGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/test/defendantNationalIdExistsGuard.spec.ts new file mode 100644 index 000000000000..07ae3267b24d --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/test/defendantNationalIdExistsGuard.spec.ts @@ -0,0 +1,125 @@ +import { uuid } from 'uuidv4' + +import { + BadRequestException, + ExecutionContext, + NotFoundException, +} from '@nestjs/common' + +import { DefendantNationalIdExistsGuard } from '../defendantNationalIdExists.guard' + +interface Then { + result: boolean + error: Error +} + +type GivenWhenThen = () => Promise + +describe('Defendant National Id Exists Guard', () => { + const mockRequest = jest.fn() + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + givenWhenThen = async (): Promise => { + const guard = new DefendantNationalIdExistsGuard() + const then = {} as Then + + try { + then.result = guard.canActivate({ + switchToHttp: () => ({ getRequest: mockRequest }), + } as unknown as ExecutionContext) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe('defendant exists', () => { + const caseId = uuid() + const defendantId = uuid() + const defendantNationalId = uuid() + const defendant = { + id: defendantId, + nationalId: defendantNationalId, + caseId, + } + const theCase = { id: caseId, defendants: [defendant] } + const request = { + params: { caseId, defendantNationalId }, + case: theCase, + defendant: undefined, + } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce(request) + + then = await givenWhenThen() + }) + + it('should activate', () => { + expect(then.result).toBe(true) + expect(request.defendant).toBe(defendant) + }) + }) + + describe('defendant does not exist', () => { + const caseId = uuid() + const defendantId = uuid() + const theCase = { + id: caseId, + defendants: [{ id: defendantId, nationalId: uuid(), caseId }], + } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ + params: { caseId, defendantNationalId: uuid() }, + case: theCase, + }) + + then = await givenWhenThen() + }) + + it('should throw NotFoundException', () => { + expect(then.error).toBeInstanceOf(NotFoundException) + expect(then.error.message).toBe( + `Defendant with given national id of case ${caseId} does not exist`, + ) + }) + }) + + describe('missing case', () => { + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ params: {} }) + + then = await givenWhenThen() + }) + + it('should throw BadRequestException', () => { + expect(then.error).toBeInstanceOf(BadRequestException) + expect(then.error.message).toBe('Missing case') + }) + }) + + describe('missing defendant id', () => { + const caseId = uuid() + const theCase = { id: caseId, defendants: [] } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ params: { caseId }, case: theCase }) + + then = await givenWhenThen() + }) + + it('should throw BadRequestException', () => { + expect(then.error).toBeInstanceOf(BadRequestException) + expect(then.error.message).toBe('Missing defendant national id') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/index.ts b/apps/judicial-system/backend/src/app/modules/defendant/index.ts index d9914e3dcda4..6b6c33d19c71 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/index.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/index.ts @@ -1,6 +1,10 @@ export { Defendant } from './models/defendant.model' export { DefendantService } from './defendant.service' -export { CivilClaimant } from './models/civilClaimant.model' export { DefendantExistsGuard } from './guards/defendantExists.guard' export { CurrentDefendant } from './guards/defendant.decorator' +export { DefendantEventLog } from './models/defendantEventLog.model' + +export { CivilClaimant } from './models/civilClaimant.model' export { CivilClaimantService } from './civilClaimant.service' +export { CivilClaimantExistsGuard } from './guards/civilClaimantExists.guard' +export { CurrentCivilClaimant } from './guards/civilClaimaint.decorator' 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 f7dc724065a6..92511ff786d0 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 @@ -17,12 +17,18 @@ import { messageEndpoint, MessageType, } from '@island.is/judicial-system/message' +import { + indictmentCases, + investigationCases, + restrictionCases, +} from '@island.is/judicial-system/types' -import { Case, CaseExistsGuard, CurrentCase } from '../case' -import { DeliverDefendantToCourtDto } from './dto/deliverDefendantToCourt.dto' +import { Case, CaseExistsGuard, CaseTypeGuard, CurrentCase } from '../case' +import { DeliverDto } from './dto/deliver.dto' import { InternalUpdateDefendantDto } from './dto/internalUpdateDefendant.dto' import { CurrentDefendant } from './guards/defendant.decorator' import { DefendantExistsGuard } from './guards/defendantExists.guard' +import { DefendantNationalIdExistsGuard } from './guards/defendantNationalIdExists.guard' import { Defendant } from './models/defendant.model' import { DeliverResponse } from './models/deliver.response' import { DefendantService } from './defendant.service' @@ -36,7 +42,10 @@ export class InternalDefendantController { @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} - @UseGuards(DefendantExistsGuard) + @UseGuards( + new CaseTypeGuard([...restrictionCases, ...investigationCases]), + DefendantExistsGuard, + ) @Post( `${messageEndpoint[MessageType.DELIVERY_TO_COURT_DEFENDANT]}/:defendantId`, ) @@ -49,7 +58,7 @@ export class InternalDefendantController { @Param('defendantId') defendantId: string, @CurrentCase() theCase: Case, @CurrentDefendant() defendant: Defendant, - @Body() deliverDefendantToCourtDto: DeliverDefendantToCourtDto, + @Body() deliverDefendantToCourtDto: DeliverDto, ): Promise { this.logger.debug( `Delivering defendant ${defendantId} of case ${caseId} to court`, @@ -62,25 +71,53 @@ export class InternalDefendantController { ) } + @UseGuards(new CaseTypeGuard(indictmentCases), DefendantNationalIdExistsGuard) @Patch('defense/:defendantNationalId') @ApiOkResponse({ type: Defendant, description: 'Updates defendant information by case and national id', }) - async updateDefendant( + updateDefendant( @Param('caseId') caseId: string, - @Param('defendantNationalId') defendantNationalId: string, + @Param('defendantNationalId') _: string, @CurrentCase() theCase: Case, + @CurrentDefendant() defendant: Defendant, @Body() updatedDefendantChoice: InternalUpdateDefendantDto, ): Promise { this.logger.debug(`Updating defendant info for ${caseId}`) - const updatedDefendant = await this.defendantService.updateByNationalId( - theCase.id, - defendantNationalId, + return this.defendantService.updateRestricted( + theCase, + defendant, updatedDefendantChoice, ) + } + + @UseGuards(new CaseTypeGuard(indictmentCases), DefendantExistsGuard) + @Post( + `${ + messageEndpoint[MessageType.DELIVERY_TO_COURT_INDICTMENT_DEFENDER] + }/:defendantId`, + ) + @ApiOkResponse({ + type: DeliverResponse, + description: 'Delivers indictment case defender info to court', + }) + deliverIndictmentDefenderToCourt( + @Param('caseId') caseId: string, + @Param('defendantId') defendantId: string, + @CurrentCase() theCase: Case, + @CurrentDefendant() defendant: Defendant, + @Body() deliverDto: DeliverDto, + ): Promise { + this.logger.debug( + `Delivering defender info for defendant ${defendantId} of case ${caseId} to court`, + ) - return updatedDefendant + return this.defendantService.deliverIndictmentDefenderToCourt( + theCase, + defendant, + deliverDto.user, + ) } } 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 be827e0468b0..bc31334d6bdd 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 @@ -23,6 +23,7 @@ import { import { Case } from '../../case/models/case.model' import { Subpoena } from '../../subpoena/models/subpoena.model' +import { DefendantEventLog } from './defendantEventLog.model' @Table({ tableName: 'defendant', @@ -195,4 +196,12 @@ export class Defendant extends Model { @Column({ type: DataType.BOOLEAN, allowNull: true }) @ApiPropertyOptional({ type: Boolean }) caseFilesSharedWithDefender?: boolean + + @Column({ type: DataType.BOOLEAN, allowNull: true }) + @ApiPropertyOptional({ type: Boolean }) + isSentToPrisonAdmin?: boolean + + @HasMany(() => DefendantEventLog, { foreignKey: 'defendantId' }) + @ApiPropertyOptional({ type: () => DefendantEventLog, isArray: true }) + eventLogs?: DefendantEventLog[] } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/models/defendantEventLog.model.ts b/apps/judicial-system/backend/src/app/modules/defendant/models/defendantEventLog.model.ts new file mode 100644 index 000000000000..ca0a332704ee --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/models/defendantEventLog.model.ts @@ -0,0 +1,64 @@ +import { + Column, + CreatedAt, + DataType, + ForeignKey, + Model, + Table, + UpdatedAt, +} from 'sequelize-typescript' + +import { ApiProperty } from '@nestjs/swagger' + +import { DefendantEventType } from '@island.is/judicial-system/types' + +import { Case } from '../../case/models/case.model' +import { Defendant } from './defendant.model' + +@Table({ + tableName: 'defendant_event_log', + timestamps: true, +}) +export class DefendantEventLog extends Model { + static sentToPrisonAdminDate(defendantEventLogs?: DefendantEventLog[]) { + return defendantEventLogs?.find( + (defendantEventLog) => + defendantEventLog.eventType === DefendantEventType.SENT_TO_PRISON_ADMIN, + ) + } + + @Column({ + type: DataType.UUID, + primaryKey: true, + allowNull: false, + defaultValue: DataType.UUIDV4, + }) + @ApiProperty({ type: String }) + id!: string + + @CreatedAt + @ApiProperty({ type: Date }) + created!: Date + + @UpdatedAt + @ApiProperty({ type: Date }) + modified!: Date + + @ForeignKey(() => Case) + @Column({ type: DataType.UUID, allowNull: false }) + @ApiProperty({ type: String }) + caseId!: string + + @ForeignKey(() => Defendant) + @Column({ type: DataType.UUID, allowNull: false }) + @ApiProperty({ type: String }) + defendantId!: string + + @Column({ + type: DataType.ENUM, + allowNull: false, + values: Object.values(DefendantEventType), + }) + @ApiProperty({ enum: DefendantEventType }) + eventType!: DefendantEventType +} diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/create.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/create.spec.ts new file mode 100644 index 000000000000..4684346f3209 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/create.spec.ts @@ -0,0 +1,80 @@ +import { uuid } from 'uuidv4' + +import { createTestingDefendantModule } from '../createTestingDefendantModule' + +import { Case } from '../../../case' +import { CivilClaimant } from '../../models/civilClaimant.model' + +interface Then { + result: CivilClaimant + error: Error +} + +type GivenWhenThen = (caseId?: string) => Promise + +describe('CivilClaimantController - Create', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const theCase = { id: caseId } as Case + const civilClaimantToCreate = { + caseId, + } + const createdCivilClaimant = { id: civilClaimantId, caseId } + + let mockCivilClaimantModel: typeof CivilClaimant + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { civilClaimantModel, civilClaimantController } = + await createTestingDefendantModule() + + mockCivilClaimantModel = civilClaimantModel + + const mockCreate = mockCivilClaimantModel.create as jest.Mock + mockCreate.mockResolvedValue(createdCivilClaimant) + + givenWhenThen = async () => { + const then = {} as Then + + await civilClaimantController + .create(theCase.id, theCase) + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) + + return then + } + }) + + describe('civil claimant creation', () => { + let then: Then + + beforeEach(async () => { + then = await givenWhenThen(caseId) + }) + it('should create a civil claimant', () => { + expect(mockCivilClaimantModel.create).toHaveBeenCalledWith( + civilClaimantToCreate, + ) + }) + + it('should return the created civil claimant', () => { + expect(then.result).toEqual(createdCivilClaimant) + }) + }) + + describe('civil claimant creation fails', () => { + let then: Then + + beforeEach(async () => { + const mockCreate = mockCivilClaimantModel.create as jest.Mock + mockCreate.mockRejectedValue(new Error('Test error')) + + then = await givenWhenThen(caseId) + }) + + it('should throw an error', () => { + expect(then.error).toBeInstanceOf(Error) + expect(then.error.message).toEqual('Test error') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createGuards.spec.ts new file mode 100644 index 000000000000..738952366261 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createGuards.spec.ts @@ -0,0 +1,25 @@ +import { CanActivate } from '@nestjs/common' + +import { CaseExistsGuard, CaseWriteGuard } from '../../../case' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Create guards', () => { + let guards: Array CanActivate> + const expectedGuards = [CaseExistsGuard, CaseWriteGuard] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + CivilClaimantController.prototype.create, + ) + }) + + it('should have the correct guards in the correct order', () => { + expect(guards).toHaveLength(expectedGuards.length) + + expectedGuards.forEach((expectedGuard, index) => { + const guardInstance = new guards[index]() + expect(guardInstance).toBeInstanceOf(expectedGuard) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createRolesRules.spec.ts new file mode 100644 index 000000000000..326946023e4f --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createRolesRules.spec.ts @@ -0,0 +1,35 @@ +import { + districtCourtAssistantRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../../../guards' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Create rules', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rules: any[] + + const expectedRules = [ + prosecutorRule, + prosecutorRepresentativeRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + districtCourtAssistantRule, + ] + + beforeEach(() => { + rules = Reflect.getMetadata( + 'roles-rules', + CivilClaimantController.prototype.create, + ) + }) + + it('should give permission to roles', () => { + expect(rules).toHaveLength(expectedRules.length) + expectedRules.forEach((expectedRule) => + expect(rules).toContain(expectedRule), + ) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/delete.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/delete.spec.ts new file mode 100644 index 000000000000..8a2c17bc5e10 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/delete.spec.ts @@ -0,0 +1,79 @@ +import { uuid } from 'uuidv4' + +import { createTestingDefendantModule } from '../createTestingDefendantModule' + +import { CivilClaimant } from '../../models/civilClaimant.model' +import { DeleteCivilClaimantResponse } from '../../models/deleteCivilClaimant.response' + +interface Then { + result: DeleteCivilClaimantResponse + error: Error +} + +type GivenWhenThen = ( + caseId?: string, + civilClaimaintId?: string, +) => Promise + +describe('CivilClaimantController - Delete', () => { + const caseId = uuid() + const civilClaimantId = uuid() + + let mockCivilClaimantModel: typeof CivilClaimant + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { civilClaimantController, civilClaimantModel } = + await createTestingDefendantModule() + + mockCivilClaimantModel = civilClaimantModel + + const mockDestroy = mockCivilClaimantModel.destroy as jest.Mock + mockDestroy.mockRejectedValue(new Error('Test error')) + + givenWhenThen = async () => { + const then = {} as Then + + try { + then.result = await civilClaimantController.delete( + caseId, + civilClaimantId, + ) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe('civil claimant deleted', () => { + let then: Then + + beforeEach(async () => { + const mockDestroy = mockCivilClaimantModel.destroy as jest.Mock + mockDestroy.mockResolvedValue(1) + + then = await givenWhenThen(caseId, civilClaimantId) + }) + it('should delete civil claimant', () => { + expect(mockCivilClaimantModel.destroy).toHaveBeenCalledWith({ + where: { caseId, id: civilClaimantId }, + }) + expect(then.result).toEqual({ deleted: true }) + }) + }) + + describe('civil claimant deletion fails', () => { + let then: Then + + beforeEach(async () => { + then = await givenWhenThen() + }) + + it('should throw Error', () => { + expect(then.error).toBeInstanceOf(Error) + expect(then.error.message).toBe('Test error') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteGuards.spec.ts new file mode 100644 index 000000000000..20e8ef89e2f6 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteGuards.spec.ts @@ -0,0 +1,25 @@ +import { CanActivate } from '@nestjs/common' + +import { CaseExistsGuard, CaseWriteGuard } from '../../../case' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Delete guards', () => { + let guards: Array CanActivate> + const expectedGuards = [CaseExistsGuard, CaseWriteGuard] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + CivilClaimantController.prototype.delete, + ) + }) + + it('should have the correct guards in the correct order', () => { + expect(guards).toHaveLength(expectedGuards.length) + + expectedGuards.forEach((expectedGuard, index) => { + const guardInstance = new guards[index]() + expect(guardInstance).toBeInstanceOf(expectedGuard) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteRolesRules.spec.ts new file mode 100644 index 000000000000..46039a55bc8b --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteRolesRules.spec.ts @@ -0,0 +1,26 @@ +import { + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../../../guards' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Delete rules', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rules: any[] + + const expectedRules = [prosecutorRule, prosecutorRepresentativeRule] + + beforeEach(() => { + rules = Reflect.getMetadata( + 'roles-rules', + CivilClaimantController.prototype.delete, + ) + }) + + it('should give permission to roles', () => { + expect(rules).toHaveLength(expectedRules.length) + expectedRules.forEach((expectedRule) => + expect(rules).toContain(expectedRule), + ) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts new file mode 100644 index 000000000000..10eba40190cc --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts @@ -0,0 +1,134 @@ +import { uuid } from 'uuidv4' + +import { MessageService, MessageType } from '@island.is/judicial-system/message' +import { CivilClaimantNotificationType } from '@island.is/judicial-system/types' + +import { createTestingDefendantModule } from '../createTestingDefendantModule' + +import { UpdateCivilClaimantDto } from '../../dto/updateCivilClaimant.dto' +import { CivilClaimant } from '../../models/civilClaimant.model' + +interface Then { + result: CivilClaimant + error: Error +} + +type GivenWhenThen = ( + caseId: string, + civilClaimantId: string, + updateData: UpdateCivilClaimantDto, +) => Promise + +describe('CivilClaimantController - Update', () => { + const caseId = uuid() + const civilClaimantId = uuid() + + let mockMessageService: MessageService + let mockCivilClaimantModel: typeof CivilClaimant + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { messageService, civilClaimantModel, civilClaimantController } = + await createTestingDefendantModule() + + mockMessageService = messageService + mockCivilClaimantModel = civilClaimantModel + + givenWhenThen = async ( + caseId: string, + civilClaimantId: string, + updateData: UpdateCivilClaimantDto, + ) => { + const then = {} as Then + + await civilClaimantController + .update(caseId, civilClaimantId, updateData) + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) + + return then + } + }) + + describe('civil claimant updated', () => { + const civilClaimantUpdate = { name: 'Updated Name' } + const updatedCivilClaimant = { + id: civilClaimantId, + caseId, + ...civilClaimantUpdate, + } + let then: Then + + beforeEach(async () => { + const mockUpdate = mockCivilClaimantModel.update as jest.Mock + mockUpdate.mockResolvedValueOnce([1, [updatedCivilClaimant]]) + + then = await givenWhenThen(caseId, civilClaimantId, civilClaimantUpdate) + }) + + it('should update the civil claimant', () => { + expect(mockCivilClaimantModel.update).toHaveBeenCalledWith( + civilClaimantUpdate, + { + where: { id: civilClaimantId, caseId }, + returning: true, + }, + ) + expect(mockMessageService.sendMessagesToQueue).not.toHaveBeenCalled() + }) + + it('should return the updated civil claimant', () => { + expect(then.result).toBe(updatedCivilClaimant) + }) + }) + + describe('civil claimant spokesperson confirmed', () => { + const civilClaimantUpdate = { isSpokespersonConfirmed: true } + const updatedCivilClaimant = { + id: civilClaimantId, + caseId, + ...civilClaimantUpdate, + } + let then: Then + + beforeEach(async () => { + const mockUpdate = mockCivilClaimantModel.update as jest.Mock + mockUpdate.mockResolvedValueOnce([1, [updatedCivilClaimant]]) + + then = await givenWhenThen(caseId, civilClaimantId, civilClaimantUpdate) + }) + + it('should queue spokesperson assigned message', () => { + expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ + { + type: MessageType.CIVIL_CLAIMANT_NOTIFICATION, + caseId, + elementId: civilClaimantId, + body: { + type: CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + }, + }, + ]) + }) + + it('should return the updated civil claimant', () => { + expect(then.result).toBe(updatedCivilClaimant) + }) + }) + + describe('civil claimant update fails', () => { + let then: Then + + beforeEach(async () => { + const mockUpdate = mockCivilClaimantModel.update as jest.Mock + mockUpdate.mockRejectedValue(new Error('Test error')) + + then = await givenWhenThen(caseId, civilClaimantId, {}) + }) + + it('should throw an error', () => { + expect(then.error).toBeInstanceOf(Error) + expect(then.error.message).toEqual('Test error') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateGuards.spec.ts new file mode 100644 index 000000000000..d333af01f86f --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateGuards.spec.ts @@ -0,0 +1,25 @@ +import { CanActivate } from '@nestjs/common' + +import { CaseExistsGuard, CaseWriteGuard } from '../../../case' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Update guards', () => { + let guards: Array CanActivate> + const expectedGuards = [CaseExistsGuard, CaseWriteGuard] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + CivilClaimantController.prototype.update, + ) + }) + + it('should have the correct guards in the correct order', () => { + expect(guards).toHaveLength(expectedGuards.length) + + expectedGuards.forEach((expectedGuard, index) => { + const guardInstance = new guards[index]() + expect(guardInstance).toBeInstanceOf(expectedGuard) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateRolesRules.spec.ts new file mode 100644 index 000000000000..38a579850480 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateRolesRules.spec.ts @@ -0,0 +1,35 @@ +import { + districtCourtAssistantRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../../../guards' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Update rules', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rules: any[] + + const expectedRules = [ + prosecutorRule, + prosecutorRepresentativeRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + districtCourtAssistantRule, + ] + + beforeEach(() => { + rules = Reflect.getMetadata( + 'roles-rules', + CivilClaimantController.prototype.update, + ) + }) + + it('should give permission to roles', () => { + expect(rules).toHaveLength(expectedRules.length) + expectedRules.forEach((expectedRule) => + expect(rules).toContain(expectedRule), + ) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts index 3b18ac9b8c8c..e6ef10b79545 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts @@ -13,10 +13,14 @@ import { MessageService } from '@island.is/judicial-system/message' import { CaseService } from '../../case' import { CourtService } from '../../court' import { UserService } from '../../user' +import { CivilClaimantController } from '../civilClaimant.controller' +import { CivilClaimantService } from '../civilClaimant.service' import { DefendantController } from '../defendant.controller' import { DefendantService } from '../defendant.service' import { InternalDefendantController } from '../internalDefendant.controller' +import { CivilClaimant } from '../models/civilClaimant.model' import { Defendant } from '../models/defendant.model' +import { DefendantEventLog } from '../models/defendantEventLog.model' jest.mock('@island.is/judicial-system/message') jest.mock('../../user/user.service') @@ -26,7 +30,11 @@ jest.mock('../../case/case.service') export const createTestingDefendantModule = async () => { const defendantModule = await Test.createTestingModule({ imports: [ConfigModule.forRoot({ load: [sharedAuthModuleConfig] })], - controllers: [DefendantController, InternalDefendantController], + controllers: [ + DefendantController, + InternalDefendantController, + CivilClaimantController, + ], providers: [ SharedAuthModule, MessageService, @@ -52,7 +60,30 @@ export const createTestingDefendantModule = async () => { findByPk: jest.fn(), }, }, + { + provide: getModelToken(CivilClaimant), + useValue: { + findOne: jest.fn(), + findAll: jest.fn(), + create: jest.fn(), + update: jest.fn(), + destroy: jest.fn(), + findByPk: jest.fn(), + }, + }, + { + provide: getModelToken(DefendantEventLog), + useValue: { + findOne: jest.fn(), + findAll: jest.fn(), + create: jest.fn(), + update: jest.fn(), + destroy: jest.fn(), + findByPk: jest.fn(), + }, + }, DefendantService, + CivilClaimantService, ], }).compile() @@ -77,6 +108,17 @@ export const createTestingDefendantModule = async () => { InternalDefendantController, ) + const civilClaimantModel = await defendantModule.resolve< + typeof CivilClaimant + >(getModelToken(CivilClaimant)) + + const civilClaimantService = + defendantModule.get(CivilClaimantService) + + const civilClaimantController = defendantModule.get( + CivilClaimantController, + ) + defendantModule.close() return { @@ -87,5 +129,8 @@ export const createTestingDefendantModule = async () => { defendantService, defendantController, internalDefendantController, + civilClaimantService, + civilClaimantController, + civilClaimantModel, } } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/update.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/update.spec.ts index 4ead03bc4b3d..34c235472f60 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/update.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/update.spec.ts @@ -22,6 +22,7 @@ interface Then { type GivenWhenThen = ( defendantUpdate: UpdateDefendantDto, + type: CaseType, courtCaseNumber?: string, ) => Promise @@ -52,6 +53,7 @@ describe('DefendantController - Update', () => { givenWhenThen = async ( defendantUpdate: UpdateDefendantDto, + type: CaseType, courtCaseNumber?: string, ) => { const then = {} as Then @@ -61,7 +63,7 @@ describe('DefendantController - Update', () => { caseId, defendantId, user, - { id: caseId, courtCaseNumber, type: CaseType.INDICTMENT } as Case, + { id: caseId, courtCaseNumber, type } as Case, defendant, defendantUpdate, ) @@ -81,7 +83,7 @@ describe('DefendantController - Update', () => { const mockUpdate = mockDefendantModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([1, [updatedDefendant]]) - then = await givenWhenThen(defendantUpdate) + then = await givenWhenThen(defendantUpdate, CaseType.CUSTODY) }) it('should update the defendant without queuing', () => { @@ -102,7 +104,7 @@ describe('DefendantController - Update', () => { const mockUpdate = mockDefendantModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([1, [updatedDefendant]]) - await givenWhenThen(defendantUpdate, uuid()) + await givenWhenThen(defendantUpdate, CaseType.INDICTMENT, uuid()) }) it('should not queue', () => { @@ -118,7 +120,7 @@ describe('DefendantController - Update', () => { const mockUpdate = mockDefendantModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([1, [updatedDefendant]]) - await givenWhenThen(defendantUpdate, uuid()) + await givenWhenThen(defendantUpdate, CaseType.CUSTODY, uuid()) }) it('should queue messages', () => { @@ -141,7 +143,7 @@ describe('DefendantController - Update', () => { const mockUpdate = mockDefendantModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([1, [updatedDefendant]]) - await givenWhenThen(defendantUpdate, uuid()) + await givenWhenThen(defendantUpdate, CaseType.TELECOMMUNICATIONS, uuid()) }) it('should queue messages', () => { @@ -162,29 +164,6 @@ describe('DefendantController - Update', () => { }) }) - describe(`defendant's defender email changed after case is delivered to court`, () => { - const defendantUpdate = { defenderEmail: uuid() } - const updatedDefendant = { ...defendant, ...defendantUpdate } - - beforeEach(async () => { - const mockUpdate = mockDefendantModel.update as jest.Mock - mockUpdate.mockResolvedValueOnce([1, [updatedDefendant]]) - - await givenWhenThen(defendantUpdate, uuid()) - }) - - it('should queue messages', () => { - expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ - { - type: MessageType.DELIVERY_TO_COURT_DEFENDANT, - user, - caseId, - elementId: defendantId, - }, - ]) - }) - }) - describe.each([ { isDefenderChoiceConfirmed: true, shouldSendEmail: true }, { isDefenderChoiceConfirmed: false, shouldSendEmail: false }, @@ -202,12 +181,18 @@ describe('DefendantController - Update', () => { const mockUpdate = mockDefendantModel.update as jest.Mock mockUpdate.mockResolvedValueOnce([1, [updatedDefendant]]) - await givenWhenThen(defendantUpdate, uuid()) + await givenWhenThen(defendantUpdate, CaseType.INDICTMENT, uuid()) }) if (shouldSendEmail) { - it('should queue message if defender has been confirmed', () => { + it('should queue messages if defender has been confirmed', () => { expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ + { + type: MessageType.DELIVERY_TO_COURT_INDICTMENT_DEFENDER, + user, + caseId, + elementId: defendantId, + }, { type: MessageType.DEFENDANT_NOTIFICATION, caseId, @@ -224,11 +209,63 @@ describe('DefendantController - Update', () => { }, ) + describe('defendant in indictment is sent to prison admin', () => { + const defendantUpdate = { isSentToPrisonAdmin: true } + const updatedDefendant = { ...defendant, ...defendantUpdate } + + beforeEach(async () => { + const mockUpdate = mockDefendantModel.update as jest.Mock + mockUpdate.mockResolvedValueOnce([1, [updatedDefendant]]) + + await givenWhenThen(defendantUpdate, CaseType.INDICTMENT, caseId) + }) + + it('should queue messages', () => { + expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ + { + type: MessageType.DEFENDANT_NOTIFICATION, + caseId, + elementId: defendantId, + body: { + type: DefendantNotificationType.INDICTMENT_SENT_TO_PRISON_ADMIN, + }, + }, + ]) + expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledTimes(1) + }) + }) + + describe('defendant in indictment is withdrawn from prison admin', () => { + const defendantUpdate = { isSentToPrisonAdmin: false } + const updatedDefendant = { ...defendant, ...defendantUpdate } + + beforeEach(async () => { + const mockUpdate = mockDefendantModel.update as jest.Mock + mockUpdate.mockResolvedValueOnce([1, [updatedDefendant]]) + + await givenWhenThen(defendantUpdate, CaseType.INDICTMENT, caseId) + }) + + it('should queue messages for indictment withdrawn from prison admin', () => { + expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ + { + type: MessageType.DEFENDANT_NOTIFICATION, + caseId, + elementId: defendantId, + body: { + type: DefendantNotificationType.INDICTMENT_WITHDRAWN_FROM_PRISON_ADMIN, + }, + }, + ]) + expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledTimes(1) + }) + }) + describe('defendant update fails', () => { let then: Then beforeEach(async () => { - then = await givenWhenThen({}) + then = await givenWhenThen({}, CaseType.CUSTODY) }) it('should throw Error', () => { diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/deliverDefendantToCourtGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/deliverDefendantToCourtGuards.spec.ts new file mode 100644 index 000000000000..7bd257fe4077 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/deliverDefendantToCourtGuards.spec.ts @@ -0,0 +1,29 @@ +import { + investigationCases, + restrictionCases, +} from '@island.is/judicial-system/types' + +import { CaseTypeGuard } from '../../../case' +import { DefendantExistsGuard } from '../../guards/defendantExists.guard' +import { InternalDefendantController } from '../../internalDefendant.controller' + +describe('InternalDefendantController - Deliver defendant to court guards', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let guards: any[] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + InternalDefendantController.prototype.deliverDefendantToCourt, + ) + }) + + it('should have the right guard configuration', () => { + expect(guards).toHaveLength(2) + expect(guards[0]).toBeInstanceOf(CaseTypeGuard) + expect(guards[0]).toEqual({ + allowedCaseTypes: [...restrictionCases, ...investigationCases], + }) + expect(new guards[1]()).toBeInstanceOf(DefendantExistsGuard) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentDefenderInfoToCourt.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/deliverIndictmentDefenderToCourt.spec.ts similarity index 56% rename from apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentDefenderInfoToCourt.spec.ts rename to apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/deliverIndictmentDefenderToCourt.spec.ts index f60117749089..5aa71689f3cf 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentDefenderInfoToCourt.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/deliverIndictmentDefenderToCourt.spec.ts @@ -2,11 +2,12 @@ import { uuid } from 'uuidv4' import { CaseType, User } from '@island.is/judicial-system/types' -import { createTestingCaseModule } from '../createTestingCaseModule' +import { createTestingDefendantModule } from '../createTestingDefendantModule' +import { Case } from '../../../case' import { CourtService } from '../../../court' import { DeliverDto } from '../../dto/deliver.dto' -import { Case } from '../../models/case.model' +import { Defendant } from '../../models/defendant.model' import { DeliverResponse } from '../../models/deliver.response' interface Then { @@ -16,56 +17,64 @@ interface Then { type GivenWhenThen = ( caseId: string, + defendantId: string, theCase: Case, + defendant: Defendant, body: DeliverDto, ) => Promise -describe('InternalCaseController - Deliver indictment defender info to court', () => { +describe('InternalDefendantController - Deliver indictment defender to court', () => { const user = { id: uuid() } as User const caseId = uuid() + const defendantId = uuid() const courtName = uuid() const courtCaseNumber = uuid() + const defendant = { + id: defendantId, + name: 'Test Ákærði', + nationalId: '1234567890', + defenderNationalId: '1234567899', + defenderName: 'Test Verjandi', + defenderEmail: 'defenderEmail', + } as Defendant const theCase = { id: caseId, type: CaseType.INDICTMENT, court: { name: courtName }, courtCaseNumber, - defendants: [ - { - name: 'Test Ákærði', - nationalId: '1234567890', - defenderNationalId: '1234567899', - defenderName: 'Test Verjandi', - defenderEmail: 'defenderEmail', - }, - { - name: 'Test Ákærði 2', - nationalId: '1234567891', - defenderNationalId: '1234567898', - defenderName: 'Test Verjandi 2', - defenderEmail: 'defenderEmail2', - }, - ], + defendants: [defendant], } as Case let mockCourtService: jest.Mocked let givenWhenThen: GivenWhenThen beforeEach(async () => { - const { courtService, internalCaseController } = - await createTestingCaseModule() + const { courtService, internalDefendantController } = + await createTestingDefendantModule() mockCourtService = courtService as jest.Mocked mockCourtService.updateIndictmentCaseWithDefenderInfo.mockResolvedValue( uuid(), ) - givenWhenThen = async (caseId: string, theCase: Case, body: DeliverDto) => { + givenWhenThen = async ( + caseId: string, + defendantId: string, + theCase: Case, + defendant: Defendant, + body: DeliverDto, + ) => { const then = {} as Then - await internalCaseController - .deliverIndictmentDefenderInfoToCourt(caseId, theCase, body) + await internalDefendantController + .deliverIndictmentDefenderToCourt( + caseId, + defendantId, + theCase, + defendant, + body, + ) .then((result) => (then.result = result)) .catch((error) => (then.error = error)) @@ -74,7 +83,9 @@ describe('InternalCaseController - Deliver indictment defender info to court', ( }) it('should deliver the defender information to court', async () => { - const then = await givenWhenThen(caseId, theCase, { user }) + const then = await givenWhenThen(caseId, defendantId, theCase, defendant, { + user, + }) expect( mockCourtService.updateIndictmentCaseWithDefenderInfo, @@ -83,7 +94,9 @@ describe('InternalCaseController - Deliver indictment defender info to court', ( caseId, courtName, courtCaseNumber, - theCase.defendants, + defendant.nationalId, + defendant.defenderName, + defendant.defenderEmail, ) expect(then.result).toEqual({ delivered: true }) @@ -96,7 +109,9 @@ describe('InternalCaseController - Deliver indictment defender info to court', ( error, ) - const then = await givenWhenThen(caseId, theCase, { user }) + const then = await givenWhenThen(caseId, defendantId, theCase, defendant, { + user, + }) expect(then.result).toEqual({ delivered: false }) }) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/deliverIndictmentDefenderToCourtGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/deliverIndictmentDefenderToCourtGuards.spec.ts new file mode 100644 index 000000000000..8e61e07902e8 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/deliverIndictmentDefenderToCourtGuards.spec.ts @@ -0,0 +1,26 @@ +import { indictmentCases } from '@island.is/judicial-system/types' + +import { CaseTypeGuard } from '../../../case' +import { DefendantExistsGuard } from '../../guards/defendantExists.guard' +import { InternalDefendantController } from '../../internalDefendant.controller' + +describe('InternalDefendantController - Deliver indictment defender to court guards', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let guards: any[] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + InternalDefendantController.prototype.deliverIndictmentDefenderToCourt, + ) + }) + + it('should have the right guard configuration', () => { + expect(guards).toHaveLength(2) + expect(guards[0]).toBeInstanceOf(CaseTypeGuard) + expect(guards[0]).toEqual({ + allowedCaseTypes: indictmentCases, + }) + expect(new guards[1]()).toBeInstanceOf(DefendantExistsGuard) + }) +}) 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 ae0ad03605fb..8ad9cc1ec040 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 @@ -3,7 +3,6 @@ import { CanActivate } from '@nestjs/common' import { TokenGuard } from '@island.is/judicial-system/auth' import { CaseExistsGuard } from '../../../case' -import { DefendantExistsGuard } from '../../guards/defendantExists.guard' import { InternalDefendantController } from '../../internalDefendant.controller' describe('InternalDefendantController - guards', () => { @@ -41,16 +40,4 @@ describe('InternalDefendantController - guards', () => { expect(guard).toBeInstanceOf(CaseExistsGuard) }) }) - - 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/defendant/test/internalDefendantController/updateDefendant.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/updateDefendant.spec.ts new file mode 100644 index 000000000000..1d6c8e73cf85 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/updateDefendant.spec.ts @@ -0,0 +1,77 @@ +import { uuid } from 'uuidv4' + +import { createTestingDefendantModule } from '../createTestingDefendantModule' + +import { Case } from '../../../case' +import { InternalUpdateDefendantDto } from '../../dto/internalUpdateDefendant.dto' +import { Defendant } from '../../models/defendant.model' + +interface Then { + result: Defendant + error: Error +} + +type GivenWhenThen = () => Promise + +describe('InternalDefendantController - Update defendant', () => { + const caseId = uuid() + const defendantId = uuid() + const defendantNationalId = uuid() + const update = { somefield: 'somevalue' } as InternalUpdateDefendantDto + const updatedDefendant = { + id: defendantId, + nationalId: defendantNationalId, + ...update, + } + let mockDefendantModel: typeof Defendant + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const defendant = { + id: defendantId, + nationalId: defendantNationalId, + } as Defendant + const { defendantModel, internalDefendantController } = + await createTestingDefendantModule() + + mockDefendantModel = defendantModel + + const mockUpdate = mockDefendantModel.update as jest.Mock + mockUpdate.mockRejectedValue(new Error('Some error')) + + givenWhenThen = async () => { + const then = {} as Then + + await internalDefendantController + .updateDefendant( + caseId, + defendantNationalId, + { id: caseId, defendants: [defendant] } as Case, + defendant, + update, + ) + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) + + return then + } + }) + + describe('update defendant', () => { + let then: Then + + beforeEach(async () => { + const mockUpdate = mockDefendantModel.update as jest.Mock + mockUpdate.mockResolvedValue([1, [updatedDefendant]]) + + then = await givenWhenThen() + }) + it('should update the defendant', async () => { + expect(mockDefendantModel.update).toHaveBeenCalledWith( + { ...update, isDefenderChoiceConfirmed: false }, + { where: { id: defendantId, caseId }, returning: true }, + ) + expect(then.result).toEqual(updatedDefendant) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/updateDefendantGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/updateDefendantGuards.spec.ts new file mode 100644 index 000000000000..a11f2f6b280b --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/updateDefendantGuards.spec.ts @@ -0,0 +1,26 @@ +import { indictmentCases } from '@island.is/judicial-system/types' + +import { CaseTypeGuard } from '../../../case' +import { DefendantNationalIdExistsGuard } from '../../guards/defendantNationalIdExists.guard' +import { InternalDefendantController } from '../../internalDefendant.controller' + +describe('InternalDefendantController - Update defendant guards', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let guards: any[] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + InternalDefendantController.prototype.updateDefendant, + ) + }) + + it('should have the right guard configuration', () => { + expect(guards).toHaveLength(2) + expect(guards[0]).toBeInstanceOf(CaseTypeGuard) + expect(guards[0]).toEqual({ + allowedCaseTypes: indictmentCases, + }) + expect(new guards[1]()).toBeInstanceOf(DefendantNationalIdExistsGuard) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/file/dto/createFile.dto.ts b/apps/judicial-system/backend/src/app/modules/file/dto/createFile.dto.ts index de31caf5e46b..6afc3c7a6ef3 100644 --- a/apps/judicial-system/backend/src/app/modules/file/dto/createFile.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/file/dto/createFile.dto.ts @@ -6,6 +6,7 @@ import { IsNumber, IsOptional, IsString, + MaxLength, } from 'class-validator' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' @@ -15,6 +16,7 @@ import { CaseFileCategory } from '@island.is/judicial-system/types' export class CreateFileDto { @IsNotEmpty() @IsString() + @MaxLength(255) @ApiProperty({ type: String }) readonly type!: string @@ -25,6 +27,7 @@ export class CreateFileDto { @IsNotEmpty() @IsString() + @MaxLength(255) @ApiProperty({ type: String }) readonly key!: string @@ -35,6 +38,7 @@ export class CreateFileDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly policeCaseNumber?: string @@ -56,11 +60,13 @@ export class CreateFileDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly policeFileId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly userGeneratedFilename?: string } diff --git a/apps/judicial-system/backend/src/app/modules/file/dto/createPresignedPost.dto.ts b/apps/judicial-system/backend/src/app/modules/file/dto/createPresignedPost.dto.ts index 1dc58cf5f6db..adc7d4691a1d 100644 --- a/apps/judicial-system/backend/src/app/modules/file/dto/createPresignedPost.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/file/dto/createPresignedPost.dto.ts @@ -1,15 +1,17 @@ -import { IsNotEmpty, IsString } from 'class-validator' +import { IsNotEmpty, IsString, MaxLength } from 'class-validator' import { ApiProperty } from '@nestjs/swagger' export class CreatePresignedPostDto { @IsNotEmpty() @IsString() + @MaxLength(255) @ApiProperty({ type: String }) readonly fileName!: string @IsNotEmpty() @IsString() + @MaxLength(255) @ApiProperty({ type: String }) readonly type!: string } diff --git a/apps/judicial-system/backend/src/app/modules/file/dto/updateFile.dto.ts b/apps/judicial-system/backend/src/app/modules/file/dto/updateFile.dto.ts index f645b71bf9bb..69a1ec9ca52a 100644 --- a/apps/judicial-system/backend/src/app/modules/file/dto/updateFile.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/file/dto/updateFile.dto.ts @@ -7,6 +7,7 @@ import { IsOptional, IsString, IsUUID, + MaxLength, Min, ValidateIf, ValidateNested, @@ -22,6 +23,7 @@ export class UpdateFileDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly userGeneratedFilename?: string 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 9982d5c839c3..54626a9f5700 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 @@ -34,7 +34,6 @@ import { districtCourtAssistantRule, districtCourtJudgeRule, districtCourtRegistrarRule, - prisonSystemStaffRule, prosecutorRepresentativeRule, prosecutorRule, publicProsecutorStaffRule, @@ -82,6 +81,7 @@ export class FileController { courtOfAppealsJudgeRule, courtOfAppealsRegistrarRule, courtOfAppealsAssistantRule, + publicProsecutorStaffRule, ) @Post('file/url') @ApiCreatedResponse({ @@ -108,6 +108,7 @@ export class FileController { courtOfAppealsJudgeRule, courtOfAppealsRegistrarRule, courtOfAppealsAssistantRule, + publicProsecutorStaffRule, ) @Post('file') @ApiCreatedResponse({ diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFileRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFileRolesRules.spec.ts index ddd88b1ea428..1321f5423d70 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFileRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFileRolesRules.spec.ts @@ -7,6 +7,7 @@ import { districtCourtRegistrarRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { FileController } from '../../file.controller' @@ -22,7 +23,7 @@ describe('FileController - Create case file rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(8) + expect(rules).toHaveLength(9) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) expect(rules).toContain(districtCourtJudgeRule) @@ -31,5 +32,6 @@ describe('FileController - Create case file rules', () => { expect(rules).toContain(courtOfAppealsJudgeRule) expect(rules).toContain(courtOfAppealsRegistrarRule) expect(rules).toContain(courtOfAppealsAssistantRule) + expect(rules).toContain(publicProsecutorStaffRule) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPostRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPostRolesRules.spec.ts index 46c5c1a7f766..04768bb581f7 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPostRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPostRolesRules.spec.ts @@ -7,6 +7,7 @@ import { districtCourtRegistrarRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { FileController } from '../../file.controller' @@ -22,7 +23,7 @@ describe('FileController - Create presigned post rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(8) + expect(rules).toHaveLength(9) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) expect(rules).toContain(districtCourtJudgeRule) @@ -31,5 +32,6 @@ describe('FileController - Create presigned post rules', () => { expect(rules).toContain(courtOfAppealsJudgeRule) expect(rules).toContain(courtOfAppealsRegistrarRule) expect(rules).toContain(courtOfAppealsAssistantRule) + expect(rules).toContain(publicProsecutorStaffRule) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts b/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts index 103ce2051ea3..0559ffa35277 100644 --- a/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts @@ -4,6 +4,7 @@ import { IsObject, IsOptional, IsString, + MaxLength, } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' @@ -14,11 +15,13 @@ import { IndictmentCountOffense } from '@island.is/judicial-system/types' export class UpdateIndictmentCountDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly policeCaseNumber?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly vehicleRegistrationNumber?: string diff --git a/apps/judicial-system/backend/src/app/modules/notification/baseNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/baseNotification.service.ts index b06c0242d66f..82c66351821c 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/baseNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/baseNotification.service.ts @@ -9,7 +9,7 @@ import type { ConfigType } from '@island.is/nest/config' import { NotificationType } from '@island.is/judicial-system/types' -import { stripHtmlTags } from '../../formatters' +import { filterWhitelistEmails, stripHtmlTags } from '../../formatters' import { notifications } from '../../messages' import { EventService } from '../event' import { DeliverResponse } from './models/deliver.response' @@ -53,6 +53,29 @@ export abstract class BaseNotificationService { }) } + private async handleWhitelist(recipients: string[]): Promise { + const whitelist = this.formatMessage(notifications.emailWhitelist) + const whitelistDomains = this.formatMessage( + notifications.emailWhitelistDomains, + ) + + const whitelistedEmails = filterWhitelistEmails( + recipients, + whitelistDomains, + whitelist, + ) + + if (whitelistedEmails.length === 0) { + this.logger.warn('No whitelisted emails found in recipients') + } + + if (whitelistedEmails.length !== recipients?.length) { + this.logger.warn('Some emails missing from whitelist') + } + + return whitelistedEmails + } + protected async sendEmail( subject: string, html: string, @@ -64,7 +87,11 @@ export abstract class BaseNotificationService { try { // This is to handle a comma separated list of emails // We use the first one as the main recipient and the rest as CC - const recipients = recipientEmail ? recipientEmail.split(',') : undefined + let recipients = recipientEmail ? recipientEmail.split(',') : undefined + + if (this.config.shouldUseWhitelist && recipients) { + recipients = await this.handleWhitelist(recipients) + } html = html.match(/
{defenderHasAccessToRVG, select, true {Hægt er að nálgast málið á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast málið hjá dómstólnum}}.', - description: - 'Body of the notification when a defender is assigned a confirmed as a defender in indictment cases', - }), -} diff --git a/apps/judicial-system/backend/src/app/modules/notification/dto/civilClaimantNotification.dto.ts b/apps/judicial-system/backend/src/app/modules/notification/dto/civilClaimantNotification.dto.ts new file mode 100644 index 000000000000..5557dbdfe354 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/dto/civilClaimantNotification.dto.ts @@ -0,0 +1,12 @@ +import { IsEnum, IsNotEmpty } from 'class-validator' + +import { ApiProperty } from '@nestjs/swagger' + +import { CivilClaimantNotificationType } from '@island.is/judicial-system/types' + +export class CivilClaimantNotificationDto { + @IsNotEmpty() + @IsEnum(CivilClaimantNotificationType) + @ApiProperty({ enum: CivilClaimantNotificationType }) + readonly type!: CivilClaimantNotificationType +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts b/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts index f5def84647e3..5e03c0f784b3 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts @@ -18,19 +18,28 @@ import { } from '@island.is/judicial-system/message' import { Case, CaseHasExistedGuard, CurrentCase } from '../case' -import { CurrentDefendant, Defendant, DefendantExistsGuard } from '../defendant' +import { + CivilClaimant, + CivilClaimantExistsGuard, + CurrentCivilClaimant, + CurrentDefendant, + Defendant, + DefendantExistsGuard, +} from '../defendant' import { SubpoenaExistsGuard } from '../subpoena' import { CaseNotificationDto } from './dto/caseNotification.dto' +import { CivilClaimantNotificationDto } from './dto/civilClaimantNotification.dto' import { DefendantNotificationDto } from './dto/defendantNotification.dto' import { InstitutionNotificationDto } from './dto/institutionNotification.dto' import { NotificationDispatchDto } from './dto/notificationDispatch.dto' import { SubpoenaNotificationDto } from './dto/subpoenaNotification.dto' import { DeliverResponse } from './models/deliver.response' -import { CaseNotificationService } from './caseNotification.service' -import { DefendantNotificationService } from './defendantNotification.service' -import { InstitutionNotificationService } from './institutionNotification.service' +import { CaseNotificationService } from './services/caseNotification/caseNotification.service' +import { CivilClaimantNotificationService } from './services/civilClaimantNotification/civilClaimantNotification.service' +import { DefendantNotificationService } from './services/defendantNotification/defendantNotification.service' +import { InstitutionNotificationService } from './services/institutionNotification/institutionNotification.service' +import { SubpoenaNotificationService } from './services/subpoenaNotification/subpoenaNotification.service' import { NotificationDispatchService } from './notificationDispatch.service' -import { SubpoenaNotificationService } from './subpoenaNotification.service' @UseGuards(TokenGuard) @Controller('api/internal') @@ -42,6 +51,7 @@ export class InternalNotificationController { private readonly institutionNotificationService: InstitutionNotificationService, private readonly subpoenaNotificationService: SubpoenaNotificationService, private readonly defendantNotificationService: DefendantNotificationService, + private readonly civilClaimantNotificationService: CivilClaimantNotificationService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -118,7 +128,35 @@ export class InternalNotificationController { return this.defendantNotificationService.sendDefendantNotification( notificationDto.type, + theCase, defendant, + ) + } + + @Post( + `case/:caseId/${ + messageEndpoint[MessageType.CIVIL_CLAIMANT_NOTIFICATION] + }/:civilClaimantId`, + ) + @UseGuards(CaseHasExistedGuard, CivilClaimantExistsGuard) + @ApiCreatedResponse({ + type: DeliverResponse, + description: 'Sends civil claimant related notifications', + }) + sendCivilClaimantNotification( + @Param('caseId') caseId: string, + @Param('civilClaimantId') civilClaimantId: string, + @CurrentCase() theCase: Case, + @CurrentCivilClaimant() civilClaimant: CivilClaimant, + @Body() notificationDto: CivilClaimantNotificationDto, + ): Promise { + this.logger.debug( + `Sending ${notificationDto.type} notification for civil claimant ${civilClaimantId} and case ${caseId}`, + ) + + return this.civilClaimantNotificationService.sendCivilClaimantNotification( + notificationDto.type, + civilClaimant, theCase, ) } diff --git a/apps/judicial-system/backend/src/app/modules/notification/notification.config.ts b/apps/judicial-system/backend/src/app/modules/notification/notification.config.ts index f863a518fad2..aecd5dabb6c9 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/notification.config.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/notification.config.ts @@ -5,6 +5,8 @@ export const notificationModuleConfig = defineConfig({ load: (env) => ({ production: env.optional('NODE_ENV') === 'production', courtOfAppealsId: '4676f08b-aab4-4b4f-a366-697540788088', + shouldUseWhitelist: + env.required('CONTENTFUL_ENVIRONMENT', 'test') !== 'master', email: { fromEmail: env.required('EMAIL_FROM', 'ben10@omnitrix.is'), fromName: env.required('EMAIL_FROM_NAME', 'Réttarvörslugátt'), @@ -12,6 +14,10 @@ export const notificationModuleConfig = defineConfig({ replyToName: env.required('EMAIL_REPLY_TO_NAME', 'Réttarvörslugátt'), prisonEmail: env.required('PRISON_EMAIL', ''), prisonAdminEmail: env.required('PRISON_ADMIN_EMAIL', ''), + prisonAdminIndictmentEmails: env.required( + 'PRISON_ADMIN_INDICTMENT_EMAILS', + '', + ), courtsEmails: env.requiredJSON('COURTS_EMAILS', {}) as { [key: string]: string }, diff --git a/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts b/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts index 6dcb837ace05..101978e3d3ba 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts @@ -17,14 +17,15 @@ import { UserModule, } from '../index' import { Notification } from './models/notification.model' -import { CaseNotificationService } from './caseNotification.service' -import { DefendantNotificationService } from './defendantNotification.service' -import { InstitutionNotificationService } from './institutionNotification.service' +import { CaseNotificationService } from './services/caseNotification/caseNotification.service' +import { CivilClaimantNotificationService } from './services/civilClaimantNotification/civilClaimantNotification.service' +import { DefendantNotificationService } from './services/defendantNotification/defendantNotification.service' +import { InstitutionNotificationService } from './services/institutionNotification/institutionNotification.service' +import { SubpoenaNotificationService } from './services/subpoenaNotification/subpoenaNotification.service' import { InternalNotificationController } from './internalNotification.controller' import { NotificationController } from './notification.controller' import { NotificationService } from './notification.service' import { NotificationDispatchService } from './notificationDispatch.service' -import { SubpoenaNotificationService } from './subpoenaNotification.service' @Module({ imports: [ @@ -43,12 +44,13 @@ import { SubpoenaNotificationService } from './subpoenaNotification.service' ], controllers: [NotificationController, InternalNotificationController], providers: [ - NotificationService, CaseNotificationService, - NotificationDispatchService, + CivilClaimantNotificationService, + DefendantNotificationService, InstitutionNotificationService, + NotificationService, + NotificationDispatchService, SubpoenaNotificationService, - DefendantNotificationService, ], }) export class NotificationModule {} diff --git a/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/services/caseNotification/caseNotification.service.ts similarity index 96% rename from apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts rename to apps/judicial-system/backend/src/app/modules/notification/services/caseNotification/caseNotification.service.ts index beca473bbbb5..0c4cbba076cb 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/services/caseNotification/caseNotification.service.ts @@ -30,7 +30,6 @@ import { lowercase, } from '@island.is/judicial-system/formatters' import { - AdvocateType, CaseAppealRulingDecision, CaseCustodyRestrictions, CaseDecision, @@ -50,7 +49,6 @@ import { } from '@island.is/judicial-system/types' import { - formatAdvocateAssignedEmailNotification, formatCourtHeadsUpSmsNotification, formatCourtIndictmentReadyForCourtEmailNotification, formatCourtOfAppealJudgeAssignedEmailNotification, @@ -70,20 +68,20 @@ import { formatProsecutorCourtDateEmailNotification, formatProsecutorReadyForCourtEmailNotification, formatProsecutorReceivedByCourtSmsNotification, -} from '../../formatters' -import { notifications } from '../../messages' -import { type Case, DateLog } from '../case' -import { CourtService } from '../court' +} from '../../../../formatters' +import { notifications } from '../../../../messages' +import { type Case, DateLog } from '../../../case' +import { CourtService } from '../../../court' import { type CivilClaimant, type Defendant, DefendantService, -} from '../defendant' -import { EventService } from '../event' -import { DeliverResponse } from './models/deliver.response' -import { Notification, Recipient } from './models/notification.model' -import { BaseNotificationService } from './baseNotification.service' -import { notificationModuleConfig } from './notification.config' +} from '../../../defendant' +import { EventService } from '../../../event' +import { BaseNotificationService } from '../../baseNotification.service' +import { DeliverResponse } from '../../models/deliver.response' +import { Notification, Recipient } from '../../models/notification.model' +import { notificationModuleConfig } from '../../notification.config' interface Attachment { filename: string @@ -1506,17 +1504,7 @@ export class CaseNotificationService extends BaseNotificationService { if (!advocateEmail) { return false } - if (isIndictmentCase(theCase.type)) { - const hasSentNotificationBefore = this.hasReceivedNotification( - CaseNotificationType.ADVOCATE_ASSIGNED, - advocateEmail, - theCase.notifications, - ) - - if (hasSentNotificationBefore) { - return false - } - } else if (isInvestigationCase(theCase.type)) { + if (isInvestigationCase(theCase.type)) { const isDefenderIncludedInSessionArrangements = theCase.sessionArrangements && [ @@ -1527,7 +1515,7 @@ export class CaseNotificationService extends BaseNotificationService { if (!isDefenderIncludedInSessionArrangements) { return false } - } else { + } else if (isRequestCase(theCase.type)) { const hasDefenderBeenNotified = this.hasReceivedNotification( [ CaseNotificationType.READY_FOR_COURT, @@ -1546,70 +1534,12 @@ export class CaseNotificationService extends BaseNotificationService { return true } - private sendAdvocateAssignedNotification( - theCase: Case, - advocateType: AdvocateType, - advocateNationalId?: string, - advocateName?: string, - advocateEmail?: string, - ): Promise { - const { subject, body } = formatAdvocateAssignedEmailNotification( - this.formatMessage, - theCase, - advocateType, - advocateNationalId && - formatDefenderRoute(this.config.clientUrl, theCase.type, theCase.id), - ) - - return this.sendEmail( - subject, - body, - advocateName, - advocateEmail, - undefined, - Boolean(advocateNationalId) === false, - ) - } - private async sendAdvocateAssignedNotifications( theCase: Case, ): Promise { const promises: Promise[] = [] - if (isIndictmentCase(theCase.type)) { - if (theCase.civilClaimants) { - for (const civilClaimant of theCase.civilClaimants) { - const { - spokespersonEmail, - spokespersonIsLawyer, - spokespersonName, - spokespersonNationalId, - hasSpokesperson, - } = civilClaimant - - const shouldSend = - hasSpokesperson && - this.shouldSendAdvocateAssignedNotification( - theCase, - spokespersonEmail, - ) - - if (shouldSend === true) { - promises.push( - this.sendAdvocateAssignedNotification( - theCase, - spokespersonIsLawyer - ? AdvocateType.LAWYER - : AdvocateType.LEGAL_RIGHTS_PROTECTOR, - spokespersonNationalId, - spokespersonName, - spokespersonEmail, - ), - ) - } - } - } - } else if (DateLog.arraignmentDate(theCase.dateLogs)?.date) { + if (DateLog.arraignmentDate(theCase.dateLogs)?.date) { const shouldSend = this.shouldSendAdvocateAssignedNotification( theCase, theCase.defenderEmail, diff --git a/apps/judicial-system/backend/src/app/modules/notification/defendantNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/services/civilClaimantNotification/civilClaimantNotification.service.ts similarity index 50% rename from apps/judicial-system/backend/src/app/modules/notification/defendantNotification.service.ts rename to apps/judicial-system/backend/src/app/modules/notification/services/civilClaimantNotification/civilClaimantNotification.service.ts index 5249a63c693d..ab8744a8dbd6 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/defendantNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/services/civilClaimantNotification/civilClaimantNotification.service.ts @@ -13,22 +13,20 @@ import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' import { type ConfigType } from '@island.is/nest/config' import { DEFENDER_INDICTMENT_ROUTE } from '@island.is/judicial-system/consts' -import { - DefendantNotificationType, - isIndictmentCase, -} from '@island.is/judicial-system/types' - -import { Case } from '../case' -import { Defendant } from '../defendant' -import { EventService } from '../event' -import { DeliverResponse } from './models/deliver.response' -import { Notification, Recipient } from './models/notification.model' -import { BaseNotificationService } from './baseNotification.service' -import { strings } from './defendantNotification.strings' -import { notificationModuleConfig } from './notification.config' +import { capitalize } from '@island.is/judicial-system/formatters' +import { CivilClaimantNotificationType } from '@island.is/judicial-system/types' + +import { Case } from '../../../case' +import { CivilClaimant } from '../../../defendant' +import { EventService } from '../../../event' +import { BaseNotificationService } from '../../baseNotification.service' +import { DeliverResponse } from '../../models/deliver.response' +import { Notification, Recipient } from '../../models/notification.model' +import { notificationModuleConfig } from '../../notification.config' +import { strings } from './civilClaimantNotification.strings' @Injectable() -export class DefendantNotificationService extends BaseNotificationService { +export class CivilClaimantNotificationService extends BaseNotificationService { constructor( @InjectModel(Notification) notificationModel: typeof Notification, @@ -50,35 +48,38 @@ export class DefendantNotificationService extends BaseNotificationService { } private async sendEmails( - defendant: Defendant, + civilClaimant: CivilClaimant, theCase: Case, - notificationType: DefendantNotificationType, + notificationType: CivilClaimantNotificationType, subject: MessageDescriptor, body: MessageDescriptor, ) { - const courtName = theCase.court?.name - const defenderHasAccessToRVG = !!defendant.defenderNationalId + const courtName = capitalize(theCase.court?.name) + const courtCaseNumber = theCase.courtCaseNumber + const spokespersonHasAccessToRVG = !!civilClaimant.spokespersonNationalId const formattedSubject = this.formatMessage(subject, { courtName, + courtCaseNumber, }) const formattedBody = this.formatMessage(body, { courtName, - courtCaseNumber: theCase.courtCaseNumber, - defenderHasAccessToRVG, + courtCaseNumber, + spokespersonHasAccessToRVG, + spokespersonIsLawyer: civilClaimant.spokespersonIsLawyer, linkStart: `
`, linkEnd: '', }) const promises: Promise[] = [] - if (defendant.defenderEmail) { + if (civilClaimant.isSpokespersonConfirmed) { promises.push( this.sendEmail( formattedSubject, formattedBody, - defendant.defenderName, - defendant.defenderEmail, + civilClaimant.spokespersonName, + civilClaimant.spokespersonEmail, undefined, true, ), @@ -90,44 +91,46 @@ export class DefendantNotificationService extends BaseNotificationService { return this.recordNotification(theCase.id, notificationType, recipients) } - private shouldSendDefenderAssignedNotification( + private shouldSendSpokespersonAssignedNotification( theCase: Case, - defendant: Defendant, + civilClaimant: CivilClaimant, ): boolean { - if (!defendant.defenderEmail || !defendant.isDefenderChoiceConfirmed) { + if ( + !civilClaimant.spokespersonEmail || + !civilClaimant.isSpokespersonConfirmed + ) { return false } - if (isIndictmentCase(theCase.type)) { - const hasSentNotificationBefore = this.hasReceivedNotification( - DefendantNotificationType.DEFENDER_ASSIGNED, - defendant.defenderEmail, - theCase.notifications, - ) + const hasSentNotificationBefore = this.hasReceivedNotification( + CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + civilClaimant.spokespersonEmail, + theCase.notifications, + ) - if (!hasSentNotificationBefore) { - return true - } + if (!hasSentNotificationBefore) { + return true } + return false } - private async sendDefenderAssignedNotification( - defendant: Defendant, + private async sendSpokespersonAssignedNotification( + civilClaimant: CivilClaimant, theCase: Case, ): Promise { - const shouldSend = this.shouldSendDefenderAssignedNotification( + const shouldSend = this.shouldSendSpokespersonAssignedNotification( theCase, - defendant, + civilClaimant, ) if (shouldSend) { return this.sendEmails( - defendant, + civilClaimant, theCase, - DefendantNotificationType.DEFENDER_ASSIGNED, - strings.defenderAssignedSubject, - strings.defenderAssignedBody, + CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + strings.civilClaimantSpokespersonAssignedSubject, + strings.civilClaimantSpokespersonAssignedBody, ) } @@ -136,13 +139,13 @@ export class DefendantNotificationService extends BaseNotificationService { } private sendNotification( - notificationType: DefendantNotificationType, - defendant: Defendant, + notificationType: CivilClaimantNotificationType, + civilClaimant: CivilClaimant, theCase: Case, ): Promise { switch (notificationType) { - case DefendantNotificationType.DEFENDER_ASSIGNED: - return this.sendDefenderAssignedNotification(defendant, theCase) + case CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED: + return this.sendSpokespersonAssignedNotification(civilClaimant, theCase) default: throw new InternalServerErrorException( `Invalid notification type: ${notificationType}`, @@ -150,15 +153,15 @@ export class DefendantNotificationService extends BaseNotificationService { } } - async sendDefendantNotification( - type: DefendantNotificationType, - defendant: Defendant, + async sendCivilClaimantNotification( + type: CivilClaimantNotificationType, + civilClaimant: CivilClaimant, theCase: Case, ): Promise { await this.refreshFormatMessage() try { - return await this.sendNotification(type, defendant, theCase) + return await this.sendNotification(type, civilClaimant, theCase) } catch (error) { this.logger.error('Failed to send notification', error) diff --git a/apps/judicial-system/backend/src/app/modules/notification/services/civilClaimantNotification/civilClaimantNotification.strings.ts b/apps/judicial-system/backend/src/app/modules/notification/services/civilClaimantNotification/civilClaimantNotification.strings.ts new file mode 100644 index 000000000000..d9beac015345 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/services/civilClaimantNotification/civilClaimantNotification.strings.ts @@ -0,0 +1,17 @@ +import { defineMessage } from '@formatjs/intl' + +export const strings = { + civilClaimantSpokespersonAssignedSubject: defineMessage({ + id: 'judicial.system.backend:civil_claimant_notifications.spokesperson_assigned_subject', + defaultMessage: '{courtName} - aðgangur að máli', + description: + 'Subject of the notification when a civil claimant spokesperson is assigned and confirmed', + }), + civilClaimantSpokespersonAssignedBody: defineMessage({ + id: 'judicial.system.backend:civil_claimant_notifications.indictment_assigned_body', + defaultMessage: + '{courtName} hefur skráð þig {spokespersonIsLawyer, select, true {lögmann einkaréttarkröfuhafa} other {réttargæslumann einkaréttarkröfuhafa}} í máli {courtCaseNumber}.

{spokespersonHasAccessToRVG, select, true {Sjá nánar á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast málið hjá dómstólnum.}}.', + description: + 'Body of the notification when a civil claimant spokesperson is assigned and confirmed', + }), +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/services/defendantNotification/defendantNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/services/defendantNotification/defendantNotification.service.ts new file mode 100644 index 000000000000..041073d9d4b2 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/services/defendantNotification/defendantNotification.service.ts @@ -0,0 +1,291 @@ +import { + Inject, + Injectable, + InternalServerErrorException, +} from '@nestjs/common' +import { InjectModel } from '@nestjs/sequelize' + +import { IntlService } from '@island.is/cms-translations' +import { EmailService } from '@island.is/email-service' +import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' +import { type ConfigType } from '@island.is/nest/config' + +import { + DEFENDER_INDICTMENT_ROUTE, + ROUTE_HANDLER_ROUTE, +} from '@island.is/judicial-system/consts' +import { + DefendantNotificationType, + isIndictmentCase, +} from '@island.is/judicial-system/types' + +import { Case } from '../../../case' +import { Defendant } from '../../../defendant' +import { EventService } from '../../../event' +import { BaseNotificationService } from '../../baseNotification.service' +import { DeliverResponse } from '../../models/deliver.response' +import { Notification, Recipient } from '../../models/notification.model' +import { notificationModuleConfig } from '../../notification.config' +import { strings } from './defendantNotification.strings' + +@Injectable() +export class DefendantNotificationService extends BaseNotificationService { + constructor( + @InjectModel(Notification) + notificationModel: typeof Notification, + @Inject(notificationModuleConfig.KEY) + config: ConfigType, + @Inject(LOGGER_PROVIDER) logger: Logger, + intlService: IntlService, + emailService: EmailService, + eventService: EventService, + ) { + super( + notificationModel, + emailService, + intlService, + config, + eventService, + logger, + ) + } + + private async sendEmails( + theCase: Case, + notificationType: DefendantNotificationType, + subject: string, + body: string, + to: { name?: string; email?: string }[], + ) { + const promises: Promise[] = [] + + for (const recipient of to) { + if (recipient.email && recipient.name) { + promises.push( + this.sendEmail( + subject, + body, + recipient.name, + recipient.email, + undefined, + true, + ), + ) + } + } + + const recipients = await Promise.all(promises) + + return this.recordNotification(theCase.id, notificationType, recipients) + } + + private sendDefendantSelectedDefenderNotification( + theCase: Case, + ): Promise { + const formattedSubject = this.formatMessage( + strings.defendantSelectedDefenderSubject, + { + courtCaseNumber: theCase.courtCaseNumber, + }, + ) + + const formattedBody = this.formatMessage( + strings.defendantSelectedDefenderBody, + { + courtCaseNumber: theCase.courtCaseNumber, + linkStart: ``, + linkEnd: '', + }, + ) + + return this.sendEmails( + theCase, + DefendantNotificationType.DEFENDANT_SELECTED_DEFENDER, + formattedSubject, + formattedBody, + [ + { + name: theCase.judge?.name, + email: theCase.judge?.email, + }, + { + name: theCase.registrar?.name, + email: theCase.registrar?.email, + }, + ], + ) + } + + private shouldSendDefenderAssignedNotification( + theCase: Case, + defendant: Defendant, + ): boolean { + if (!defendant.defenderEmail || !defendant.isDefenderChoiceConfirmed) { + return false + } + + if (isIndictmentCase(theCase.type)) { + const hasSentNotificationBefore = this.hasReceivedNotification( + DefendantNotificationType.DEFENDER_ASSIGNED, + defendant.defenderEmail, + theCase.notifications, + ) + + if (!hasSentNotificationBefore) { + return true + } + } + return false + } + + private async sendDefenderAssignedNotification( + theCase: Case, + defendant: Defendant, + ): Promise { + const shouldSend = this.shouldSendDefenderAssignedNotification( + theCase, + defendant, + ) + + if (shouldSend) { + const courtName = theCase.court?.name + const defenderHasAccessToRVG = !!defendant.defenderNationalId + + const formattedSubject = this.formatMessage( + strings.defenderAssignedSubject, + { + courtName, + }, + ) + + const formattedBody = this.formatMessage(strings.defenderAssignedBody, { + courtName, + courtCaseNumber: theCase.courtCaseNumber, + defenderHasAccessToRVG, + linkStart: ``, + linkEnd: '', + }) + + return this.sendEmails( + theCase, + DefendantNotificationType.DEFENDER_ASSIGNED, + formattedSubject, + formattedBody, + [{ name: defendant.defenderName, email: defendant.defenderEmail }], + ) + } + + // Nothing should be sent so we return a successful response + return { delivered: true } + } + + private sendIndictmentSentToPrisonAdminNotification(theCase: Case) { + const formattedSubject = this.formatMessage( + strings.indictmentSentToPrisonAdminSubject, + { + courtCaseNumber: theCase.courtCaseNumber, + }, + ) + + const formattedBody = this.formatMessage( + strings.indictmentSentToPrisonAdminBody, + { + courtCaseNumber: theCase.courtCaseNumber, + linkStart: ``, + linkEnd: '', + }, + ) + + // We want to send separate emails to each recipient + const to = this.config.email.prisonAdminIndictmentEmails + .split(',') + .map((email) => email.trim()) + .map((email) => { + return { + name: 'Fangelsismálastofnun', + email, + } + }) + + return this.sendEmails( + theCase, + DefendantNotificationType.INDICTMENT_SENT_TO_PRISON_ADMIN, + formattedSubject, + formattedBody, + to, + ) + } + + private sendIndictmentWithdrawnFromPrisonAdminNotification(theCase: Case) { + const courtCaseNumber = theCase.courtCaseNumber + + const formattedSubject = this.formatMessage( + strings.indictmentWithdrawnFromPrisonAdminSubject, + { + courtCaseNumber, + }, + ) + + const formattedBody = this.formatMessage( + strings.indictmentWithdrawnFromPrisonAdminBody, + { + courtCaseNumber, + }, + ) + + // We want to send separate emails to each recipient + const to = this.config.email.prisonAdminIndictmentEmails + .split(',') + .map((email) => email.trim()) + .map((email) => { + return { + name: 'Fangelsismálastofnun', + email, + } + }) + + return this.sendEmails( + theCase, + DefendantNotificationType.INDICTMENT_WITHDRAWN_FROM_PRISON_ADMIN, + formattedSubject, + formattedBody, + to, + ) + } + + private sendNotification( + notificationType: DefendantNotificationType, + theCase: Case, + defendant: Defendant, + ): Promise { + switch (notificationType) { + case DefendantNotificationType.DEFENDANT_SELECTED_DEFENDER: + return this.sendDefendantSelectedDefenderNotification(theCase) + case DefendantNotificationType.DEFENDER_ASSIGNED: + return this.sendDefenderAssignedNotification(theCase, defendant) + case DefendantNotificationType.INDICTMENT_SENT_TO_PRISON_ADMIN: + return this.sendIndictmentSentToPrisonAdminNotification(theCase) + case DefendantNotificationType.INDICTMENT_WITHDRAWN_FROM_PRISON_ADMIN: + return this.sendIndictmentWithdrawnFromPrisonAdminNotification(theCase) + default: + throw new InternalServerErrorException( + `Invalid notification type: ${notificationType}`, + ) + } + } + + async sendDefendantNotification( + type: DefendantNotificationType, + theCase: Case, + defendant: Defendant, + ): Promise { + await this.refreshFormatMessage() + try { + return await this.sendNotification(type, theCase, defendant) + } catch (error) { + this.logger.error('Failed to send notification', error) + + return { delivered: false } + } + } +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/services/defendantNotification/defendantNotification.strings.ts b/apps/judicial-system/backend/src/app/modules/notification/services/defendantNotification/defendantNotification.strings.ts new file mode 100644 index 000000000000..02eebc37019c --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/services/defendantNotification/defendantNotification.strings.ts @@ -0,0 +1,55 @@ +import { defineMessage } from '@formatjs/intl' + +export const strings = { + defendantSelectedDefenderSubject: defineMessage({ + id: 'judicial.system.backend:defendant_notifications.defendant_selected_defender_subject', + defaultMessage: 'Val á verjanda í máli {courtCaseNumber}', + description: + 'Subject of the notification sent when the defendant defender choise in an indictment has changed', + }), + defendantSelectedDefenderBody: defineMessage({ + id: 'judicial.system.backend:defendant_notifications.defendant_selected_defender_body', + defaultMessage: + 'Verjandi hefur verið valinn í máli {courtCaseNumber}.

Sjá nánar á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}.', + description: + 'Body of the notification sent when the defendant defender choise in an indictment has changed', + }), + defenderAssignedSubject: defineMessage({ + id: 'judicial.system.backend:defendant_notifications.indictment_defender_assigned_subject', + defaultMessage: '{courtName} - aðgangur að máli', + description: + 'Subject of the notification when a defender is assigned a confirmed as a defender in indictment cases', + }), + defenderAssignedBody: defineMessage({ + id: 'judicial.system.backend:defendant_notifications.indictment_defender_assigned_body', + defaultMessage: + '{courtName} hefur skráð þig verjanda í máli {courtCaseNumber}.

{defenderHasAccessToRVG, select, true {Hægt er að nálgast málið á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast málið hjá dómstólnum}}.', + description: + 'Body of the notification when a defender is assigned a confirmed as a defender in indictment cases', + }), + indictmentSentToPrisonAdminSubject: defineMessage({ + id: 'judicial.system.backend:defendant_notifications.indictment_sent_to_prison_admin_subject', + defaultMessage: 'Mál {courtCaseNumber} til fullnustu', + description: + 'Titill í tilkynningu til FMST þegar mál er sent til fullnustu', + }), + indictmentSentToPrisonAdminBody: defineMessage({ + id: 'judicial.system.backend:defendant_notifications.indictment_sent_to_prison_admin_body', + defaultMessage: + 'Ríkissaksóknari hefur sent mál {courtCaseNumber} til fullnustu.

Skjöl málsins eru aðgengileg á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}.', + description: 'Texti í tilkynningu til FMST þegar mál er sent til fullnustu', + }), + indictmentWithdrawnFromPrisonAdminSubject: defineMessage({ + id: 'judicial.system.backend:defendant_notifications.indictment_withdrawn_from_prison_admin_subject', + defaultMessage: 'Mál {courtCaseNumber} afturkallað úr fullnustu', + description: + 'Titill í tilkynningu til FMST þegar mál er afturkallað úr fullnustu', + }), + indictmentWithdrawnFromPrisonAdminBody: defineMessage({ + id: 'judicial.system.backend:defendant_notifications.indictment_withdrawn_from_prison_admin_body', + defaultMessage: + 'Ríkissaksóknari hefur afturkallað mál {courtCaseNumber} úr fullnustu.', + description: + 'Texti í tilkynningu til FMST þegar mál er afturkallað úr fullnustu', + }), +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/institutionNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/services/institutionNotification/institutionNotification.service.ts similarity index 87% rename from apps/judicial-system/backend/src/app/modules/notification/institutionNotification.service.ts rename to apps/judicial-system/backend/src/app/modules/notification/services/institutionNotification/institutionNotification.service.ts index 82727cab9a70..8d3c9ae9af2a 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/institutionNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/services/institutionNotification/institutionNotification.service.ts @@ -12,14 +12,14 @@ import { type ConfigType } from '@island.is/nest/config' import { InstitutionNotificationType } from '@island.is/judicial-system/types' -import { InternalCaseService } from '../case' -import { EventService } from '../event' -import { type User, UserService } from '../user' -import { DeliverResponse } from './models/deliver.response' -import { Notification } from './models/notification.model' -import { BaseNotificationService } from './baseNotification.service' +import { InternalCaseService } from '../../../case' +import { EventService } from '../../../event' +import { type User, UserService } from '../../../user' +import { BaseNotificationService } from '../../baseNotification.service' +import { DeliverResponse } from '../../models/deliver.response' +import { Notification } from '../../models/notification.model' +import { notificationModuleConfig } from '../../notification.config' import { strings } from './institutionNotification.strings' -import { notificationModuleConfig } from './notification.config' @Injectable() export class InstitutionNotificationService extends BaseNotificationService { diff --git a/apps/judicial-system/backend/src/app/modules/notification/institutionNotification.strings.ts b/apps/judicial-system/backend/src/app/modules/notification/services/institutionNotification/institutionNotification.strings.ts similarity index 100% rename from apps/judicial-system/backend/src/app/modules/notification/institutionNotification.strings.ts rename to apps/judicial-system/backend/src/app/modules/notification/services/institutionNotification/institutionNotification.strings.ts diff --git a/apps/judicial-system/backend/src/app/modules/notification/subpoenaNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/services/subpoenaNotification/subpoenaNotification.service.ts similarity index 76% rename from apps/judicial-system/backend/src/app/modules/notification/subpoenaNotification.service.ts rename to apps/judicial-system/backend/src/app/modules/notification/services/subpoenaNotification/subpoenaNotification.service.ts index 13fad8b63ff5..575d73abd748 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/subpoenaNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/services/subpoenaNotification/subpoenaNotification.service.ts @@ -15,12 +15,12 @@ import { type ConfigType } from '@island.is/nest/config' import { ROUTE_HANDLER_ROUTE } from '@island.is/judicial-system/consts' import { SubpoenaNotificationType } from '@island.is/judicial-system/types' -import { Case } from '../case' -import { EventService } from '../event' -import { DeliverResponse } from './models/deliver.response' -import { Notification, Recipient } from './models/notification.model' -import { BaseNotificationService } from './baseNotification.service' -import { notificationModuleConfig } from './notification.config' +import { Case } from '../../../case' +import { EventService } from '../../../event' +import { BaseNotificationService } from '../../baseNotification.service' +import { DeliverResponse } from '../../models/deliver.response' +import { Notification, Recipient } from '../../models/notification.model' +import { notificationModuleConfig } from '../../notification.config' import { strings } from './subpoenaNotification.strings' @Injectable() @@ -56,16 +56,16 @@ export class SubpoenaNotificationService extends BaseNotificationService { courtCaseNumber: theCase.courtCaseNumber, }) + const formattedBody = this.formatMessage(body, { + courtCaseNumber: theCase.courtCaseNumber, + linkStart: ``, + linkEnd: '', + }) + const promises: Promise[] = [] for (const recipient of to) { if (recipient.email && recipient.name) { - const formattedBody = this.formatMessage(body, { - courtCaseNumber: theCase.courtCaseNumber, - linkStart: ``, - linkEnd: '', - }) - promises.push( this.sendEmail( formattedSubject, @@ -134,27 +134,6 @@ export class SubpoenaNotificationService extends BaseNotificationService { ) } - private sendDefendantSelectedDefenderNotification( - theCase: Case, - ): Promise { - return this.sendEmails( - theCase, - SubpoenaNotificationType.DEFENDANT_SELECTED_DEFENDER, - strings.defendantSelectedDefenderSubject, - strings.defendantSelectedDefenderBody, - [ - { - name: theCase.judge?.name, - email: theCase.judge?.email, - }, - { - name: theCase.registrar?.name, - email: theCase.registrar?.email, - }, - ], - ) - } - private sendNotification( type: SubpoenaNotificationType, theCase: Case, @@ -164,8 +143,6 @@ export class SubpoenaNotificationService extends BaseNotificationService { return this.sendServiceSuccessfulNotification(theCase) case SubpoenaNotificationType.SERVICE_FAILED: return this.sendServiceFailedNotification(theCase) - case SubpoenaNotificationType.DEFENDANT_SELECTED_DEFENDER: - return this.sendDefendantSelectedDefenderNotification(theCase) default: throw new InternalServerErrorException( `Invalid notification type: ${type}`, diff --git a/apps/judicial-system/backend/src/app/modules/notification/subpoenaNotification.strings.ts b/apps/judicial-system/backend/src/app/modules/notification/services/subpoenaNotification/subpoenaNotification.strings.ts similarity index 66% rename from apps/judicial-system/backend/src/app/modules/notification/subpoenaNotification.strings.ts rename to apps/judicial-system/backend/src/app/modules/notification/services/subpoenaNotification/subpoenaNotification.strings.ts index 8f08b6f475ae..0b475708668f 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/subpoenaNotification.strings.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/services/subpoenaNotification/subpoenaNotification.strings.ts @@ -27,17 +27,4 @@ export const strings = { description: 'Body of the notification sent when the serive status in an indictment has changed', }), - defendantSelectedDefenderSubject: defineMessage({ - id: 'judicial.system.backend:subpoena_notifications.defendant_selected_defender_subject', - defaultMessage: 'Val á verjanda í máli {courtCaseNumber}', - description: - 'Subject of the notification sent when the serive status in an indictment has changed', - }), - defendantSelectedDefenderBody: defineMessage({ - id: 'judicial.system.backend:subpoena_notifications.defendant_selected_defender_body', - defaultMessage: - 'Verjandi hefur verið valinn í máli {courtCaseNumber}.

Sjá nánar á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}.', - description: - 'Body of the notification sent when the serive status in an indictment has changed', - }), } diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts b/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts index a269d181001b..c5ea4ac34521 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts @@ -24,18 +24,43 @@ import { DefendantService } from '../../defendant' import { eventModuleConfig, EventService } from '../../event' import { InstitutionService } from '../../institution' import { UserService } from '../../user' -import { CaseNotificationService } from '../caseNotification.service' -import { DefendantNotificationService } from '../defendantNotification.service' -import { InstitutionNotificationService } from '../institutionNotification.service' import { InternalNotificationController } from '../internalNotification.controller' import { Notification } from '../models/notification.model' import { notificationModuleConfig } from '../notification.config' import { NotificationController } from '../notification.controller' import { NotificationService } from '../notification.service' import { NotificationDispatchService } from '../notificationDispatch.service' +import { CaseNotificationService } from '../services/caseNotification/caseNotification.service' +import { CivilClaimantNotificationService } from '../services/civilClaimantNotification/civilClaimantNotification.service' +import { DefendantNotificationService } from '../services/defendantNotification/defendantNotification.service' +import { InstitutionNotificationService } from '../services/institutionNotification/institutionNotification.service' jest.mock('@island.is/judicial-system/message') +export const createTestUsers = ( + roles: string[], +): Record< + string, + { + id: string + name: string + email: string + mobile: string + nationalId: string + } +> => + roles.reduce((acc, role) => { + const id = uuid() + acc[role] = { + id: id, + name: `${role}-${id}`, + email: `${role}-${id}@omnitrix.is`, + mobile: id, + nationalId: '1234567890', + } + return acc + }, {} as Record) + const formatMessage = createTestIntl({ onError: jest.fn(), locale: 'is-IS', @@ -104,6 +129,7 @@ export const createTestingNotificationModule = async () => { NotificationDispatchService, InstitutionNotificationService, DefendantNotificationService, + CivilClaimantNotificationService, ], }) .useMocker((token) => { diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/civilClaimantNotification/sendSpokespersonAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/civilClaimantNotification/sendSpokespersonAssignedNotifications.spec.ts new file mode 100644 index 000000000000..42bf7c2a0ea8 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/civilClaimantNotification/sendSpokespersonAssignedNotifications.spec.ts @@ -0,0 +1,167 @@ +import { uuid } from 'uuidv4' + +import { EmailService } from '@island.is/email-service' +import { ConfigType } from '@island.is/nest/config' + +import { DEFENDER_INDICTMENT_ROUTE } from '@island.is/judicial-system/consts' +import { + CaseType, + CivilClaimantNotificationType, +} from '@island.is/judicial-system/types' + +import { createTestingNotificationModule } from '../../createTestingNotificationModule' + +import { Case } from '../../../../case' +import { CivilClaimant } from '../../../../defendant' +import { CivilClaimantNotificationDto } from '../../../dto/civilClaimantNotification.dto' +import { DeliverResponse } from '../../../models/deliver.response' +import { Notification } from '../../../models/notification.model' +import { notificationModuleConfig } from '../../../notification.config' + +jest.mock('../../../../../factories') + +interface Then { + result: DeliverResponse + error: Error +} + +type GivenWhenThen = ( + caseId: string, + civilClaimantId: string, + theCase: Case, + civilClaimant: CivilClaimant, + notificationDto: CivilClaimantNotificationDto, +) => Promise + +describe('InternalNotificationController - Send spokesperson assigned notifications', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const court = { name: 'Héraðsdómur Reykjavíkur' } as Case['court'] + + let mockEmailService: EmailService + let mockConfig: ConfigType + let mockNotificationModel: typeof Notification + let givenWhenThen: GivenWhenThen + + let civilClaimantNotificationDTO: CivilClaimantNotificationDto + + beforeEach(async () => { + const { + emailService, + notificationConfig, + internalNotificationController, + notificationModel, + } = await createTestingNotificationModule() + + civilClaimantNotificationDTO = { + type: CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + } + + mockEmailService = emailService + mockConfig = notificationConfig + mockNotificationModel = notificationModel + + givenWhenThen = async ( + caseId: string, + civilClaimantId: string, + theCase: Case, + civilClaimant: CivilClaimant, + notificationDto: CivilClaimantNotificationDto, + ) => { + const then = {} as Then + + try { + then.result = + await internalNotificationController.sendCivilClaimantNotification( + caseId, + civilClaimantId, + theCase, + civilClaimant, + notificationDto, + ) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe.each([ + { isSpokespersonConfirmed: true, shouldSendEmail: true }, + { isSpokespersonConfirmed: false, shouldSendEmail: false }, + ])( + 'when sending a spokesperson assigned notification', + ({ isSpokespersonConfirmed, shouldSendEmail }) => { + const civilClaimant = { + id: civilClaimantId, + caseId, + isSpokespersonConfirmed, + spokespersonIsLawyer: true, + spokespersonNationalId: '1234567890', + spokespersonName: 'Ben 10', + spokespersonEmail: 'ben10@omnitrix.is', + } as CivilClaimant + + beforeEach(async () => { + await givenWhenThen( + caseId, + civilClaimantId, + { + id: caseId, + court, + courtCaseNumber: 'R-123-456', + type: CaseType.INDICTMENT, + civilClaimants: [civilClaimant], + hasCivilClaims: true, + } as Case, + civilClaimant, + civilClaimantNotificationDTO, + ) + }) + + test(`should ${ + shouldSendEmail ? '' : 'not ' + }send a spokesperson assigned notification`, async () => { + if (shouldSendEmail) { + expect(mockEmailService.sendEmail).toBeCalledTimes(1) + expect(mockEmailService.sendEmail).toBeCalledWith({ + from: { + name: mockConfig.email.fromName, + address: mockConfig.email.fromEmail, + }, + to: [ + { + name: civilClaimant.spokespersonName, + address: civilClaimant.spokespersonEmail, + }, + ], + replyTo: { + name: mockConfig.email.replyToName, + address: mockConfig.email.replyToEmail, + }, + attachments: undefined, + subject: `Héraðsdómur Reykjavíkur - aðgangur að máli`, + html: expect.stringContaining(DEFENDER_INDICTMENT_ROUTE), + text: expect.stringContaining( + `Héraðsdómur Reykjavíkur hefur skráð þig lögmann einkaréttarkröfuhafa í máli R-123-456`, + ), + }) + expect(mockNotificationModel.create).toHaveBeenCalledTimes(1) + expect(mockNotificationModel.create).toHaveBeenCalledWith({ + caseId, + type: civilClaimantNotificationDTO.type, + recipients: [ + { + address: civilClaimant.spokespersonEmail, + success: shouldSendEmail, + }, + ], + }) + } else { + expect(mockEmailService.sendEmail).not.toBeCalled() + } + }) + }, + ) +}) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendDefenderAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendDefenderAssignedNotifications.spec.ts index 41403f0ea3f1..5d048acbad6c 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendDefenderAssignedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendDefenderAssignedNotifications.spec.ts @@ -9,7 +9,10 @@ import { DefendantNotificationType, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../../createTestingNotificationModule' import { Case } from '../../../../case' import { Defendant } from '../../../../defendant' @@ -36,6 +39,8 @@ type GivenWhenThen = ( describe('InternalNotificationController - Send defender assigned notifications', () => { const caseId = uuid() const defendantId = uuid() + + const { defender } = createTestUsers(['defender']) const court = { name: 'Héraðsdómur Reykjavíkur' } as Case['court'] let mockEmailService: EmailService @@ -91,8 +96,8 @@ describe('InternalNotificationController - Send defender assigned notifications' const defendant = { id: defendantId, defenderNationalId: '1234567890', - defenderName: 'Defender Name', - defenderEmail: 'ben10@omnitrix.is', + defenderName: defender.name, + defenderEmail: defender.email, isDefenderChoiceConfirmed: true, } as Defendant @@ -158,7 +163,7 @@ describe('InternalNotificationController - Send defender assigned notifications' describe('when sending defender assigned notification to unconfirmed defender', () => { const defendant = { id: defendantId, - defenderEmail: 'ben101@omnitrix.is', + defenderEmail: defender.email, isDefenderChoiceConfirmed: false, } as Defendant diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendIndictmentSentToPrisonAdminNotification.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendIndictmentSentToPrisonAdminNotification.spec.ts new file mode 100644 index 000000000000..1c5f3468bbfa --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendIndictmentSentToPrisonAdminNotification.spec.ts @@ -0,0 +1,144 @@ +import { uuid } from 'uuidv4' + +import { EmailService } from '@island.is/email-service' + +import { ROUTE_HANDLER_ROUTE } from '@island.is/judicial-system/consts' +import { + CaseType, + DefendantNotificationType, +} from '@island.is/judicial-system/types' + +import { createTestingNotificationModule } from '../../createTestingNotificationModule' + +import { Case } from '../../../../case' +import { Defendant } from '../../../../defendant' +import { DefendantNotificationDto } from '../../../dto/defendantNotification.dto' +import { DeliverResponse } from '../../../models/deliver.response' +import { Notification } from '../../../models/notification.model' + +jest.mock('../../../../../factories') + +interface Then { + result: DeliverResponse + error: Error +} + +type GivenWhenThen = ( + caseId: string, + defendantId: string, + theCase: Case, + defendant: Defendant, + notificationDto: DefendantNotificationDto, +) => Promise + +describe('InternalNotificationController - Defendant - Send indictment sent to prison admin notification', () => { + const caseId = uuid() + const defendantId = uuid() + const emails = [ + 'prisonadminindictment@omnitrix.is', + 'prisonadminindictment2@omnitrix.is', + ] + + let mockEmailService: EmailService + let mockNotificationModel: typeof Notification + let defendantNotificationDTO: DefendantNotificationDto + + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + process.env.PRISON_ADMIN_INDICTMENT_EMAILS = emails.join(',') + + const { emailService, internalNotificationController, notificationModel } = + await createTestingNotificationModule() + + defendantNotificationDTO = { + type: DefendantNotificationType.INDICTMENT_SENT_TO_PRISON_ADMIN, + } + + mockEmailService = emailService + mockNotificationModel = notificationModel + + givenWhenThen = async ( + caseId: string, + defendantId: string, + theCase: Case, + defendant: Defendant, + notificationDto: DefendantNotificationDto, + ) => { + const then = {} as Then + + try { + then.result = + await internalNotificationController.sendDefendantNotification( + caseId, + defendantId, + theCase, + defendant, + notificationDto, + ) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe('when sending indictment to prison admin', () => { + const defendant = { + id: defendantId, + } as Defendant + + const theCase = { + id: caseId, + courtCaseNumber: 'S-123-456/2024', + type: CaseType.INDICTMENT, + defendants: [defendant], + } as Case + + beforeEach(async () => { + await givenWhenThen( + caseId, + defendantId, + theCase, + defendant, + defendantNotificationDTO, + ) + }) + + it('should send a notification to prison admin emails', () => { + expect(mockEmailService.sendEmail).toBeCalledTimes(emails.length) + emails.forEach((email) => { + expect(mockEmailService.sendEmail).toBeCalledWith( + expect.objectContaining({ + to: [ + { + name: 'Fangelsismálastofnun', + address: email, + }, + ], + + attachments: undefined, + subject: `Mál S-123-456/2024 til fullnustu`, + html: expect.stringContaining(ROUTE_HANDLER_ROUTE), + text: expect.stringContaining( + 'Ríkissaksóknari hefur sent mál S-123-456/2024 til fullnustu.', + ), + }), + ) + }) + }) + + it('should record notification', () => { + expect(mockNotificationModel.create).toHaveBeenCalledTimes(1) + expect(mockNotificationModel.create).toHaveBeenCalledWith({ + caseId, + type: defendantNotificationDTO.type, + recipients: emails.map((email) => ({ + address: email, + success: true, + })), + }) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendIndictmentWithdrawnFromPrisonAdminNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendIndictmentWithdrawnFromPrisonAdminNotifications.spec.ts new file mode 100644 index 000000000000..3a35e28ac53e --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendIndictmentWithdrawnFromPrisonAdminNotifications.spec.ts @@ -0,0 +1,140 @@ +import { uuid } from 'uuidv4' + +import { EmailService } from '@island.is/email-service' + +import { + CaseType, + DefendantNotificationType, +} from '@island.is/judicial-system/types' + +import { createTestingNotificationModule } from '../../createTestingNotificationModule' + +import { Case } from '../../../../case' +import { Defendant } from '../../../../defendant' +import { DefendantNotificationDto } from '../../../dto/defendantNotification.dto' +import { DeliverResponse } from '../../../models/deliver.response' +import { Notification } from '../../../models/notification.model' + +jest.mock('../../../../../factories') + +interface Then { + result: DeliverResponse + error: Error +} + +type GivenWhenThen = ( + caseId: string, + defendantId: string, + theCase: Case, + defendant: Defendant, + notificationDto: DefendantNotificationDto, +) => Promise + +describe('InternalNotificationController - Defendant - Send indictment withdrawn from prison admin notification', () => { + const caseId = uuid() + const defendantId = uuid() + const emails = [ + 'prisonadminindictment@omnitrix.is', + 'prisonadminindictment2@omnitrix.is', + ] + + let mockEmailService: EmailService + let mockNotificationModel: typeof Notification + let defendantNotificationDTO: DefendantNotificationDto + + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + process.env.PRISON_ADMIN_INDICTMENT_EMAILS = emails.join(',') + + const { emailService, internalNotificationController, notificationModel } = + await createTestingNotificationModule() + + defendantNotificationDTO = { + type: DefendantNotificationType.INDICTMENT_WITHDRAWN_FROM_PRISON_ADMIN, + } + + mockEmailService = emailService + mockNotificationModel = notificationModel + + givenWhenThen = async ( + caseId: string, + defendantId: string, + theCase: Case, + defendant: Defendant, + notificationDto: DefendantNotificationDto, + ) => { + const then = {} as Then + + try { + then.result = + await internalNotificationController.sendDefendantNotification( + caseId, + defendantId, + theCase, + defendant, + notificationDto, + ) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe('when withdrawing indictment from prison admin', () => { + const defendant = { + id: defendantId, + } as Defendant + + const theCase = { + id: caseId, + courtCaseNumber: 'S-123-456/2024', + type: CaseType.INDICTMENT, + defendants: [defendant], + } as Case + + beforeEach(async () => { + await givenWhenThen( + caseId, + defendantId, + theCase, + defendant, + defendantNotificationDTO, + ) + }) + + it('should send a notification to prison admin emails', () => { + expect(mockEmailService.sendEmail).toBeCalledTimes(emails.length) + emails.forEach((email) => { + expect(mockEmailService.sendEmail).toBeCalledWith( + expect.objectContaining({ + to: [ + { + name: 'Fangelsismálastofnun', + address: email, + }, + ], + subject: `Mál S-123-456/2024 afturkallað úr fullnustu`, + text: expect.stringContaining( + 'Ríkissaksóknari hefur afturkallað mál S-123-456/2024 úr fullnustu.', + ), + }), + ) + }) + }) + + it('should record notification', () => { + expect(mockNotificationModel.create).toHaveBeenCalledTimes(1) + expect(mockNotificationModel.create).toHaveBeenCalledWith({ + caseId, + type: defendantNotificationDTO.type, + recipients: emails.map((email) => ({ + address: email, + success: true, + })), + }) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts index eff36227b407..c8f120ffbf71 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts @@ -3,10 +3,7 @@ import { uuid } from 'uuidv4' import { EmailService } from '@island.is/email-service' import { ConfigType } from '@island.is/nest/config' -import { - DEFENDER_INDICTMENT_ROUTE, - DEFENDER_ROUTE, -} from '@island.is/judicial-system/consts' +import { DEFENDER_ROUTE } from '@island.is/judicial-system/consts' import { CaseNotificationType, CaseType, @@ -14,12 +11,14 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' import { DeliverResponse } from '../../models/deliver.response' -import { Notification } from '../../models/notification.model' import { notificationModuleConfig } from '../../notification.config' jest.mock('../../../../factories') @@ -37,21 +36,19 @@ type GivenWhenThen = ( describe('InternalNotificationController - Send defender assigned notifications', () => { const userId = uuid() + + const { defender } = createTestUsers(['defender']) + const court = { name: 'Héraðsdómur Reykjavíkur' } as Case['court'] let mockEmailService: EmailService let mockConfig: ConfigType - let mockNotificationModel: typeof Notification let givenWhenThen: GivenWhenThen let notificationDTO: CaseNotificationDto beforeEach(async () => { - const { - emailService, - notificationConfig, - notificationModel, - internalNotificationController, - } = await createTestingNotificationModule() + const { emailService, notificationConfig, internalNotificationController } = + await createTestingNotificationModule() notificationDTO = { user: { id: userId } as User, @@ -60,7 +57,6 @@ describe('InternalNotificationController - Send defender assigned notifications' mockEmailService = emailService mockConfig = notificationConfig - mockNotificationModel = notificationModel givenWhenThen = async ( caseId: string, @@ -83,120 +79,6 @@ describe('InternalNotificationController - Send defender assigned notifications' } }) - describe('when the case has civil claims and the advocate is a lawyer', () => { - const caseId = uuid() - const civilClaimant = { - hasSpokesperson: true, - spokespersonNationalId: '1234567890', - spokespersonEmail: 'recipient@gmail.com', - spokespersonName: 'John Doe', - spokespersonIsLawyer: true, - } - const theCase = { - id: caseId, - type: CaseType.INDICTMENT, - court, - courtCaseNumber: 'S-123/2022', - civilClaimants: [civilClaimant], - } as Case - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, notificationDTO) - }) - - it('should send correct email', () => { - expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(1) - expect(mockEmailService.sendEmail).toHaveBeenCalledWith({ - from: { - name: mockConfig.email.fromName, - address: mockConfig.email.fromEmail, - }, - to: [ - { - name: civilClaimant.spokespersonName, - address: civilClaimant.spokespersonEmail, - }, - ], - replyTo: { - name: mockConfig.email.replyToName, - address: mockConfig.email.replyToEmail, - }, - attachments: undefined, - subject: `Skráning í máli ${theCase.courtCaseNumber}`, - text: expect.anything(), // same as html but stripped html tags - html: `Héraðsdómur Reykjavíkur hefur skráð þig lögmann einkaréttarkröfuhafa í máli ${theCase.courtCaseNumber}.

Sjá nánar á yfirlitssíðu málsins í Réttarvörslugátt.`, - }) - }) - }) - - describe('when the case has civil claims and the advocate is a legal rights protector', () => { - const caseId = uuid() - const civilClaimant = { - hasSpokesperson: true, - spokespersonNationalId: '1234567890', - spokespersonEmail: 'recipient@gmail.com', - spokespersonName: 'John Doe', - spokespersonIsLawyer: false, - } - const theCase = { - id: caseId, - type: CaseType.INDICTMENT, - court, - courtCaseNumber: 'S-123/2022', - civilClaimants: [civilClaimant], - } as Case - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, notificationDTO) - }) - - it('should send correct email', () => { - expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(1) - expect(mockEmailService.sendEmail).toHaveBeenCalledWith({ - from: { - name: mockConfig.email.fromName, - address: mockConfig.email.fromEmail, - }, - to: [ - { - name: civilClaimant.spokespersonName, - address: civilClaimant.spokespersonEmail, - }, - ], - replyTo: { - name: mockConfig.email.replyToName, - address: mockConfig.email.replyToEmail, - }, - attachments: undefined, - subject: `Skráning í máli ${theCase.courtCaseNumber}`, - text: expect.anything(), // same as html but stripped html tags - html: `Héraðsdómur Reykjavíkur hefur skráð þig réttargæslumann einkaréttarkröfuhafa í máli ${theCase.courtCaseNumber}.

Sjá nánar á yfirlitssíðu málsins í Réttarvörslugátt.`, - }) - }) - }) - - describe('when the case has civil claims and civil claimant does not have representation', () => { - const caseId = uuid() - const civilClaimant = { - hasSpokesperson: false, - } - const theCase = { - id: caseId, - type: CaseType.INDICTMENT, - court, - courtCaseNumber: 'S-123/2022', - civilClaimants: [civilClaimant], - } as Case - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, notificationDTO) - }) - - it('should send correct email', () => { - expect(mockEmailService.sendEmail).not.toHaveBeenCalled() - }) - }) - describe('when sending assigned defender notifications in a restriction case', () => { const caseId = uuid() const theCase = { @@ -204,8 +86,8 @@ describe('InternalNotificationController - Send defender assigned notifications' type: CaseType.ADMISSION_TO_FACILITY, court, courtCaseNumber: 'R-123/2022', - defenderEmail: 'recipient@gmail.com', - defenderName: 'John Doe', + defenderEmail: defender.email, + defenderName: defender.name, defenderNationalId: '1234567890', dateLogs: [{ date: new Date(), dateType: DateType.ARRAIGNMENT_DATE }], } as Case @@ -246,8 +128,8 @@ describe('InternalNotificationController - Send defender assigned notifications' type: CaseType.ADMISSION_TO_FACILITY, court, courtCaseNumber: 'R-123/2022', - defenderEmail: 'recipient@gmail.com', - defenderName: 'John Doe', + defenderEmail: defender.email, + defenderName: defender.name, dateLogs: [{ date: new Date(), dateType: DateType.ARRAIGNMENT_DATE }], } as Case @@ -287,8 +169,8 @@ describe('InternalNotificationController - Send defender assigned notifications' type: CaseType.PHONE_TAPPING, court, courtCaseNumber: 'R-123/2022', - defenderEmail: 'recipient@gmail.com', - defenderName: 'John Doe', + defenderEmail: defender.email, + defenderName: defender.name, dateLogs: [{ date: new Date(), dateType: DateType.ARRAIGNMENT_DATE }], } as Case diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealCompletedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealCompletedNotifications.spec.ts index bc9c0a684778..df4807508115 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealCompletedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealCompletedNotifications.spec.ts @@ -12,7 +12,10 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -29,25 +32,24 @@ type GivenWhenThen = ( ) => Promise describe('InternalNotificationController - Send appeal completed notifications', () => { + const { prosecutor, defender, judge, courtOfAppeals } = createTestUsers([ + 'prosecutor', + 'defender', + 'judge', + 'courtOfAppeals', + ]) const userId = uuid() const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const defenderName = uuid() - const defenderEmail = uuid() - const judgeName = uuid() - const judgeEmail = uuid() const courtCaseNumber = uuid() const appealCaseNumber = uuid() const courtId = uuid() - const courtOfAppealsEmail = uuid() let mockEmailService: EmailService let mockConfig: ConfigType let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.COURTS_EMAILS = `{"4676f08b-aab4-4b4f-a366-697540788088":"${courtOfAppealsEmail}"}` + process.env.COURTS_EMAILS = `{"4676f08b-aab4-4b4f-a366-697540788088":"${courtOfAppeals.email}"}` const { emailService, notificationConfig, internalNotificationController } = await createTestingNotificationModule() @@ -71,12 +73,12 @@ describe('InternalNotificationController - Send appeal completed notifications', decision: CaseDecision.ACCEPTING, appealRulingDecision: appealRulingDecision ?? CaseAppealRulingDecision.ACCEPTING, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, - judge: { name: judgeName, email: judgeEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, + judge: { name: judge.name, email: judge.email }, court: { name: 'Héraðsdómur Reykjavíkur' }, defenderNationalId, - defenderName: defenderName, - defenderEmail: defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, courtCaseNumber, appealCaseNumber, courtId: courtId, @@ -102,14 +104,14 @@ describe('InternalNotificationController - Send appeal completed notifications', it('should send notifications', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Úrskurður í landsréttarmáli ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur úrskurðað í máli ${appealCaseNumber} (héraðsdómsmál nr. ${courtCaseNumber}). Niðurstaða Landsréttar: Staðfest. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Úrskurður í landsréttarmáli ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur úrskurðað í máli ${appealCaseNumber} (héraðsdómsmál nr. ${courtCaseNumber}). Niðurstaða Landsréttar: Staðfest. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -140,7 +142,7 @@ describe('InternalNotificationController - Send appeal completed notifications', ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Úrskurður í landsréttarmáli ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur úrskurðað í máli ${appealCaseNumber} (héraðsdómsmál nr. ${courtCaseNumber}). Niðurstaða Landsréttar: Staðfest. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -159,7 +161,7 @@ describe('InternalNotificationController - Send appeal completed notifications', it('should send notification without a link to defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Úrskurður í landsréttarmáli ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur úrskurðað í máli ${appealCaseNumber} (héraðsdómsmál nr. ${courtCaseNumber}). Niðurstaða Landsréttar: Staðfest. Hægt er að nálgast gögn málsins hjá Héraðsdómi Reykjavíkur ef þau hafa ekki þegar verið afhent.`, }), @@ -178,14 +180,14 @@ describe('InternalNotificationController - Send appeal completed notifications', it('should send notification about discontinuance', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Niðurfelling máls ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur móttekið afturköllun á kæru í máli ${courtCaseNumber}. Landsréttarmálið ${appealCaseNumber} hefur verið fellt niður. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Niðurfelling máls ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur móttekið afturköllun á kæru í máli ${courtCaseNumber}. Landsréttarmálið ${appealCaseNumber} hefur verið fellt niður.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealFilesUpdatedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealFilesUpdatedNotifications.spec.ts index 1cc6f4f9e31a..d50f5d2a5707 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealFilesUpdatedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealFilesUpdatedNotifications.spec.ts @@ -8,7 +8,10 @@ import { UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -21,22 +24,17 @@ interface Then { type GivenWhenThen = (defenderNationalId?: string) => Promise describe('InternalNotificationController - Send appeal case files updated notifications', () => { + const { assistant, judge1, judge2, judge3 } = createTestUsers([ + 'assistant', + 'judge1', + 'judge2', + 'judge3', + ]) const userId = uuid() const caseId = uuid() const courtCaseNumber = uuid() const appealCaseNumber = uuid() const receivedDate = new Date() - const assistantName = uuid() - const assistantEmail = uuid() - const judgeName1 = uuid() - const judgeEmail1 = uuid() - const judgeId1 = uuid() - const judgeName2 = uuid() - const judgeEmail2 = uuid() - const judgeId2 = uuid() - const judgeName3 = uuid() - const judgeEmail3 = uuid() - const judgeId3 = uuid() let mockEmailService: EmailService let givenWhenThen: GivenWhenThen @@ -59,27 +57,26 @@ describe('InternalNotificationController - Send appeal case files updated notifi appealCaseNumber, appealReceivedByCourtDate: receivedDate, appealAssistant: { - name: assistantName, - email: assistantEmail, + name: assistant.name, + email: assistant.email, role: UserRole.COURT_OF_APPEALS_ASSISTANT, }, appealJudge1: { - name: judgeName1, - email: judgeEmail1, - id: judgeId1, + name: judge1.name, + email: judge1.email, + id: judge1.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, - appealJudge1Id: judgeId1, appealJudge2: { - name: judgeName2, - email: judgeEmail2, - id: judgeId2, + name: judge2.name, + email: judge2.email, + id: judge2.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, appealJudge3: { - name: judgeName3, - email: judgeEmail3, - id: judgeId3, + name: judge3.name, + email: judge3.email, + id: judge3.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, } as Case, @@ -104,7 +101,7 @@ describe('InternalNotificationController - Send appeal case files updated notifi it('should send notification to the assigned court of appeal judges and assistant', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: assistantName, address: assistantEmail }], + to: [{ name: assistant.name, address: assistant.email }], subject: `Ný gögn í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Ný gögn hafa borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -113,8 +110,8 @@ describe('InternalNotificationController - Send appeal case files updated notifi expect.objectContaining({ to: [ { - name: judgeName1, - address: judgeEmail1, + name: judge1.name, + address: judge1.email, }, ], subject: `Ný gögn í máli ${courtCaseNumber} (${appealCaseNumber})`, @@ -123,14 +120,14 @@ describe('InternalNotificationController - Send appeal case files updated notifi ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName2, address: judgeEmail2 }], + to: [{ name: judge2.name, address: judge2.email }], subject: `Ný gögn í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Ný gögn hafa borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName3, address: judgeEmail3 }], + to: [{ name: judge3.name, address: judge3.email }], subject: `Ný gögn í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Ný gögn hafa borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealJudgesAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealJudgesAssignedNotifications.spec.ts index b455d775f174..cf73dbe84fcd 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealJudgesAssignedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealJudgesAssignedNotifications.spec.ts @@ -8,7 +8,10 @@ import { UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -21,21 +24,16 @@ interface Then { type GivenWhenThen = (defenderNationalId?: string) => Promise describe('InternalNotificationController - Send appeal judges assigned notifications', () => { + const { judge1, judge2, judge3, assistant } = createTestUsers([ + 'judge1', + 'judge2', + 'judge3', + 'assistant', + ]) const userId = uuid() const caseId = uuid() const appealCaseNumber = uuid() const receivedDate = new Date() - const assistantName = uuid() - const assistantEmail = uuid() - const judgeName1 = uuid() - const judgeEmail1 = uuid() - const judgeId1 = uuid() - const judgeName2 = uuid() - const judgeEmail2 = uuid() - const judgeId2 = uuid() - const judgeName3 = uuid() - const judgeEmail3 = uuid() - const judgeId3 = uuid() let mockEmailService: EmailService let givenWhenThen: GivenWhenThen @@ -57,27 +55,27 @@ describe('InternalNotificationController - Send appeal judges assigned notificat appealCaseNumber, appealReceivedByCourtDate: receivedDate, appealAssistant: { - name: assistantName, - email: assistantEmail, + name: assistant.name, + email: assistant.email, role: UserRole.COURT_OF_APPEALS_ASSISTANT, }, appealJudge1: { - name: judgeName1, - email: judgeEmail1, - id: judgeId1, + name: judge1.name, + email: judge1.email, + id: judge1.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, - appealJudge1Id: judgeId1, + appealJudge1Id: judge1.id, appealJudge2: { - name: judgeName2, - email: judgeEmail2, - id: judgeId2, + name: judge2.name, + email: judge2.email, + id: judge2.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, appealJudge3: { - name: judgeName3, - email: judgeEmail3, - id: judgeId3, + name: judge3.name, + email: judge3.email, + id: judge3.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, } as Case, @@ -102,17 +100,17 @@ describe('InternalNotificationController - Send appeal judges assigned notificat it('should send notification to the judge foreperson, the two other judges and the judges assistant', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: assistantName, address: assistantEmail }], + to: [{ name: assistant.name, address: assistant.email }], subject: `Úthlutun máls nr. ${appealCaseNumber}`, - html: `Landsréttur hefur skráð þig sem aðstoðarmann dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judgeName1}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, + html: `Landsréttur hefur skráð þig sem aðstoðarmann dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judge1.name}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ to: [ { - name: judgeName1, - address: judgeEmail1, + name: judge1.name, + address: judge1.email, }, ], subject: `Úthlutun máls nr. ${appealCaseNumber}`, @@ -121,16 +119,16 @@ describe('InternalNotificationController - Send appeal judges assigned notificat ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName2, address: judgeEmail2 }], + to: [{ name: judge2.name, address: judge2.email }], subject: `Úthlutun máls nr. ${appealCaseNumber}`, - html: `Landsréttur hefur skráð þig sem dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judgeName1}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, + html: `Landsréttur hefur skráð þig sem dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judge1.name}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName3, address: judgeEmail3 }], + to: [{ name: judge3.name, address: judge3.email }], subject: `Úthlutun máls nr. ${appealCaseNumber}`, - html: `Landsréttur hefur skráð þig sem dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judgeName1}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, + html: `Landsréttur hefur skráð þig sem dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judge1.name}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(then.result).toEqual({ delivered: true }) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealReceivedByCourtNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealReceivedByCourtNotifications.spec.ts index 27a132aa27a7..96a4f7fe0016 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealReceivedByCourtNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealReceivedByCourtNotifications.spec.ts @@ -10,7 +10,10 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -23,14 +26,14 @@ interface Then { type GivenWhenThen = (defenderNationalId?: string) => Promise describe('InternalNotificationController - Send appeal received by court notifications', () => { - const courtOfAppealsEmail = uuid() + const { coa, defender, prosecutor } = createTestUsers([ + 'coa', + 'defender', + 'prosecutor', + ]) + const userId = uuid() const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const prosecutorMobileNumber = uuid() - const defenderName = uuid() - const defenderEmail = uuid() const courtCaseNumber = uuid() const receivedDate = new Date() @@ -40,7 +43,7 @@ describe('InternalNotificationController - Send appeal received by court notific let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.COURTS_EMAILS = `{"4676f08b-aab4-4b4f-a366-697540788088":"${courtOfAppealsEmail}"}` + process.env.COURTS_EMAILS = `{"4676f08b-aab4-4b4f-a366-697540788088":"${coa.email}"}` const { emailService, smsService, internalNotificationController } = await createTestingNotificationModule() @@ -57,14 +60,14 @@ describe('InternalNotificationController - Send appeal received by court notific { id: caseId, prosecutor: { - name: prosecutorName, - email: prosecutorEmail, - mobileNumber: prosecutorMobileNumber, + name: prosecutor.name, + email: prosecutor.email, + mobileNumber: prosecutor.mobile, }, court: { name: 'Héraðsdómur Reykjavíkur' }, defenderNationalId, - defenderName: defenderName, - defenderEmail: defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, courtCaseNumber, appealReceivedByCourtDate: receivedDate, } as Case, @@ -92,7 +95,7 @@ describe('InternalNotificationController - Send appeal received by court notific to: [ { name: 'Landsréttur', - address: courtOfAppealsEmail, + address: coa.email, }, ], subject: `Upplýsingar vegna kæru í máli ${courtCaseNumber}`, @@ -104,7 +107,7 @@ describe('InternalNotificationController - Send appeal received by court notific ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Upplýsingar vegna kæru í máli ${courtCaseNumber}`, html: `Kæra í máli ${courtCaseNumber} hefur borist Landsrétti. Frestur til að skila greinargerð er til ${formatDate( getStatementDeadline(receivedDate), @@ -114,7 +117,7 @@ describe('InternalNotificationController - Send appeal received by court notific ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Upplýsingar vegna kæru í máli ${courtCaseNumber}`, html: `Kæra í máli ${courtCaseNumber} hefur borist Landsrétti. Frestur til að skila greinargerð er til ${formatDate( getStatementDeadline(receivedDate), @@ -127,7 +130,7 @@ describe('InternalNotificationController - Send appeal received by court notific it('should send sms notification to prosecutor', () => { expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [prosecutorMobileNumber], + [prosecutor.mobile], `Kæra í máli ${courtCaseNumber} hefur borist Landsrétti. Frestur til að skila greinargerð er til ${formatDate( getStatementDeadline(receivedDate), 'PPPp', @@ -146,7 +149,7 @@ describe('InternalNotificationController - Send appeal received by court notific it('should send notification to prosecutor and defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Upplýsingar vegna kæru í máli ${courtCaseNumber}`, html: `Kæra í máli ${courtCaseNumber} hefur borist Landsrétti. Frestur til að skila greinargerð er til ${formatDate( getStatementDeadline(receivedDate), @@ -156,7 +159,7 @@ describe('InternalNotificationController - Send appeal received by court notific ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Upplýsingar vegna kæru í máli ${courtCaseNumber}`, html: `Kæra í máli ${courtCaseNumber} hefur borist Landsrétti. Frestur til að skila greinargerð er til ${formatDate( getStatementDeadline(receivedDate), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealStatementNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealStatementNotifications.spec.ts index 0f043505f796..a920284e6a9e 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealStatementNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealStatementNotifications.spec.ts @@ -9,7 +9,10 @@ import { UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -26,22 +29,22 @@ type GivenWhenThen = ( ) => Promise describe('InternalNotificationController - Send appeal statement notifications', () => { + const roles = [ + 'prosecutor', + 'defender', + 'assistant', + 'judge1', + 'judge2', + 'judge3', + ] + + const { prosecutor, defender, assistant, judge1, judge2, judge3 } = + createTestUsers(roles) + const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const defenderName = uuid() - const defenderEmail = uuid() const courtCaseNumber = uuid() const receivedDate = new Date() const appealCaseNumber = uuid() - const assistantName = uuid() - const assistantEmail = uuid() - const judgeName1 = uuid() - const judgeEmail1 = uuid() - const judgeName2 = uuid() - const judgeEmail2 = uuid() - const judgeName3 = uuid() - const judgeEmail3 = uuid() let mockEmailService: EmailService @@ -65,16 +68,16 @@ describe('InternalNotificationController - Send appeal statement notifications', caseId, { id: caseId, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, court: { name: 'Héraðsdómur Reykjavíkur' }, defenderNationalId, - defenderName: defenderName, - defenderEmail: defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, courtCaseNumber, appealReceivedByCourtDate: receivedDate, appealCaseNumber, - appealAssistant: { name: assistantName, email: assistantEmail }, - appealJudge1: { name: judgeName1, email: judgeEmail1 }, + appealAssistant: { name: assistant.name, email: assistant.email }, + appealJudge1: { name: judge1.name, email: judge1.email }, } as Case, { user, @@ -110,13 +113,13 @@ describe('InternalNotificationController - Send appeal statement notifications', }) it('should send notification to appeals court and defender', () => { - expectCourtEmail(assistantName, assistantEmail) - expectCourtEmail(judgeName1, judgeEmail1) - expectCourtEmail(judgeName2, judgeEmail2) - expectCourtEmail(judgeName3, judgeEmail3) + expectCourtEmail(assistant.name, assistant.email) + expectCourtEmail(judge1.name, judge1.email) + expectCourtEmail(judge2.name, judge2.email) + expectCourtEmail(judge3.name, judge3.email) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Ný greinargerð í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -140,13 +143,13 @@ describe('InternalNotificationController - Send appeal statement notifications', }) it('should send notification to appeals court and defender', () => { - expectCourtEmail(assistantName, assistantEmail) - expectCourtEmail(judgeName1, judgeEmail1) - expectCourtEmail(judgeName2, judgeEmail2) - expectCourtEmail(judgeName3, judgeEmail3) + expectCourtEmail(assistant.name, assistant.email) + expectCourtEmail(judge1.name, judge1.email) + expectCourtEmail(judge2.name, judge2.email) + expectCourtEmail(judge3.name, judge3.email) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Ný greinargerð í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins hjá Héraðsdómi Reykjavíkur ef þau hafa ekki þegar verið afhent.`, }), @@ -167,13 +170,13 @@ describe('InternalNotificationController - Send appeal statement notifications', }) it('should send notification to appeals court and prosecutor', () => { - expectCourtEmail(assistantName, assistantEmail) - expectCourtEmail(judgeName1, judgeEmail1) - expectCourtEmail(judgeName2, judgeEmail2) - expectCourtEmail(judgeName3, judgeEmail3) + expectCourtEmail(assistant.name, assistant.email) + expectCourtEmail(judge1.name, judge1.email) + expectCourtEmail(judge2.name, judge2.email) + expectCourtEmail(judge3.name, judge3.email) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Ný greinargerð í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -198,7 +201,7 @@ describe('InternalNotificationController - Send appeal statement notifications', it('should send notification to defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Ný greinargerð í máli ${courtCaseNumber}`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -220,7 +223,7 @@ describe('InternalNotificationController - Send appeal statement notifications', it('should send notification to defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Ný greinargerð í máli ${courtCaseNumber}`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins hjá Héraðsdómi Reykjavíkur ef þau hafa ekki þegar verið afhent.`, }), @@ -239,7 +242,7 @@ describe('InternalNotificationController - Send appeal statement notifications', it('should send notification to prosecutor', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Ný greinargerð í máli ${courtCaseNumber}`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealToCourtOfAppealsNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealToCourtOfAppealsNotifications.spec.ts index 2119328928c1..ae50c9e3c325 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealToCourtOfAppealsNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealToCourtOfAppealsNotifications.spec.ts @@ -10,7 +10,10 @@ import { UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -23,20 +26,17 @@ interface Then { type GivenWhenThen = (user: User, defenderNationalId?: string) => Promise describe('InternalNotificationController - Send appeal to court of appeals notifications', () => { + const { prosecutor, judge, registrar, defender, court } = createTestUsers([ + 'prosecutor', + 'judge', + 'registrar', + 'defender', + 'court', + ]) + const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const prosecutorMobileNumber = uuid() - const judgeName = uuid() - const judgeEmail = uuid() - const registrarName = uuid() - const registrarEmail = uuid() - const defenderName = uuid() - const defenderEmail = uuid() + const courtCaseNumber = uuid() - const courtId = uuid() - const courtEmail = uuid() - const courtMobileNumber = uuid() let mockEmailService: EmailService let mockSmsService: SmsService @@ -44,8 +44,8 @@ describe('InternalNotificationController - Send appeal to court of appeals notif let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.COURTS_ASSISTANT_MOBILE_NUMBERS = `{"${courtId}": "${courtMobileNumber}"}` - process.env.COURTS_EMAILS = `{"${courtId}": "${courtEmail}"}` + process.env.COURTS_ASSISTANT_MOBILE_NUMBERS = `{"${court.id}": "${court.mobile}"}` + process.env.COURTS_EMAILS = `{"${court.id}": "${court.email}"}` const { emailService, smsService, internalNotificationController } = await createTestingNotificationModule() @@ -62,18 +62,18 @@ describe('InternalNotificationController - Send appeal to court of appeals notif { id: caseId, prosecutor: { - name: prosecutorName, - email: prosecutorEmail, - mobileNumber: prosecutorMobileNumber, + name: prosecutor.name, + email: prosecutor.email, + mobileNumber: prosecutor.mobile, }, - judge: { name: judgeName, email: judgeEmail }, - registrar: { name: registrarName, email: registrarEmail }, + judge: { name: judge.name, email: judge.email }, + registrar: { name: registrar.name, email: registrar.email }, court: { name: 'Héraðsdómur Reykjavíkur' }, defenderNationalId, - defenderName: defenderName, - defenderEmail: defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, courtCaseNumber, - courtId: courtId, + courtId: court.id, } as Case, { user, @@ -102,35 +102,35 @@ describe('InternalNotificationController - Send appeal to court of appeals notif it('should send notification to judge, registrar, court and defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: registrarName, address: registrarEmail }], + to: [{ name: registrar.name, address: registrar.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: 'Héraðsdómur Reykjavíkur', address: courtEmail }], + to: [{ name: 'Héraðsdómur Reykjavíkur', address: court.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [courtMobileNumber], + [court.mobile], `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Sjá nánar á rettarvorslugatt.island.is`, ) expect(then.result).toEqual({ delivered: true }) @@ -150,20 +150,20 @@ describe('InternalNotificationController - Send appeal to court of appeals notif it('should send notification to judge and defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins hjá Héraðsdómi Reykjavíkur ef þau hafa ekki þegar verið afhent.`, }), ) expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [courtMobileNumber], + [court.mobile], `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Sjá nánar á rettarvorslugatt.island.is`, ) expect(then.result).toEqual({ delivered: true }) @@ -180,24 +180,24 @@ describe('InternalNotificationController - Send appeal to court of appeals notif it('should send notifications to judge and prosecutor', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [courtMobileNumber], + [court.mobile], `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Sjá nánar á rettarvorslugatt.island.is`, ) expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [prosecutorMobileNumber], + [prosecutor.mobile], `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Sjá nánar á rettarvorslugatt.island.is`, ) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealWithdrawnNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealWithdrawnNotifications.spec.ts index 1b2be1516b1e..bc96d0d487d9 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealWithdrawnNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealWithdrawnNotifications.spec.ts @@ -5,11 +5,15 @@ import { EmailService } from '@island.is/email-service' import { CaseNotificationType, InstitutionType, + NotificationType, User, UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -27,26 +31,33 @@ type GivenWhenThen = ( ) => Promise describe('InternalNotificationController - Send appeal withdrawn notifications', () => { - const courtOfAppealsEmail = uuid() - const courtEmail = uuid() + const { + courtOfAppeals, + court, + judge, + prosecutor, + defender, + registrar, + appealAssistant, + appealJudge1, + } = createTestUsers([ + 'courtOfAppeals', + 'court', + 'judge', + 'prosecutor', + 'defender', + 'registrar', + 'appealAssistant', + 'appealJudge1', + ]) + + const courtOfAppealsEmail = courtOfAppeals.email + const courtEmail = court.email const courtId = uuid() const userId = uuid() const caseId = uuid() - const judgeName = uuid() - const judgeEmail = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const prosecutorMobileNumber = uuid() - const defenderName = uuid() - const defenderEmail = uuid() const courtCaseNumber = uuid() const receivedDate = new Date() - const registrarEmail = uuid() - const registrarName = uuid() - const appealAssistantName = uuid() - const appealAssistantEmail = uuid() - const appealJudge1Name = uuid() - const appealJudge1Email = uuid() let mockEmailService: EmailService let mockNotificationModel: typeof Notification @@ -74,22 +85,25 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', { id: caseId, prosecutor: { - name: prosecutorName, - email: prosecutorEmail, - mobileNumber: prosecutorMobileNumber, + name: prosecutor.name, + email: prosecutor.email, + mobileNumber: prosecutor.mobile, }, court: { name: 'Héraðsdómur Reykjavíkur', id: courtId }, - defenderName: defenderName, - defenderEmail: defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, courtCaseNumber, appealReceivedByCourtDate, appealAssistant: { - name: appealAssistantName, - email: appealAssistantEmail, + name: appealAssistant.name, + email: appealAssistant.email, + }, + judge: { name: judge.name, email: judge.email }, + appealJudge1: { + name: appealJudge1.name, + email: appealJudge1.email, }, - judge: { name: judgeName, email: judgeEmail }, - appealJudge1: { name: appealJudge1Name, email: appealJudge1Email }, - registrar: { name: registrarName, email: registrarEmail }, + registrar: { name: registrar.name, email: registrar.email }, notifications, } as Case, { @@ -116,7 +130,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Sækjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -140,7 +154,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to appeal assistant ', () => { expect(mockEmailService.sendEmail).not.toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: appealAssistantName, address: appealAssistantEmail }], + to: [{ name: appealAssistant.name, address: appealAssistant.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Sækjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -158,14 +172,14 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', then = await givenWhenThen(UserRole.PROSECUTOR, receivedDate, [ { caseId, - type: CaseNotificationType.APPEAL_JUDGES_ASSIGNED, + type: NotificationType.APPEAL_JUDGES_ASSIGNED, } as Notification, ]) }) it('should send notification to defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Sækjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -189,7 +203,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to appeal assistant ', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: appealAssistantName, address: appealAssistantEmail }], + to: [{ name: appealAssistant.name, address: appealAssistant.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Sækjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -198,7 +212,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to appeal judges', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: appealJudge1Name, address: appealJudge1Email }], + to: [{ name: appealJudge1.name, address: appealJudge1.email }], }), ) }) @@ -214,7 +228,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to prosecutor', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Verjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -226,7 +240,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to judge', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Verjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -236,7 +250,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to registrar', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: registrarName, address: registrarEmail }], + to: [{ name: registrar.name, address: registrar.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Verjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCaseFilesUpdatedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCaseFilesUpdatedNotifications.spec.ts index 21df0ee723b4..d57f0ea344f2 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCaseFilesUpdatedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCaseFilesUpdatedNotifications.spec.ts @@ -10,7 +10,10 @@ import { UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -23,18 +26,15 @@ interface Then { type GivenWhenThen = (user: User) => Promise describe('InternalNotificationController - Send case files updated notifications', () => { + const { prosecutor, judge, defender, spokesperson } = createTestUsers([ + 'prosecutor', + 'judge', + 'defender', + 'spokesperson', + ]) + const caseId = uuid() const courtCaseNumber = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const judgeName = uuid() - const judgeEmail = uuid() - const defenderNationalId = uuid() - const defenderName = uuid() - const defenderEmail = uuid() - const spokespersonNationalId = uuid() - const spokespersonName = uuid() - const spokespersonEmail = uuid() let mockEmailService: EmailService let givenWhenThen: GivenWhenThen @@ -55,15 +55,21 @@ describe('InternalNotificationController - Send case files updated notifications type: CaseType.INDICTMENT, courtCaseNumber, court: { name: 'Héraðsdómur Reykjavíkur' }, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, - judge: { name: judgeName, email: judgeEmail }, - defendants: [{ defenderNationalId, defenderName, defenderEmail }], + prosecutor: { name: prosecutor.name, email: prosecutor.email }, + judge: { name: judge.name, email: judge.email }, + defendants: [ + { + defenderNationalId: defender.nationalId, + defenderName: defender.name, + defenderEmail: defender.email, + }, + ], civilClaimants: [ { hasSpokesperson: true, - spokespersonNationalId, - spokespersonName, - spokespersonEmail, + spokespersonNationalId: spokesperson.nationalId, + spokespersonName: spokesperson.name, + spokespersonEmail: spokesperson.email, }, ], } as Case, @@ -92,21 +98,21 @@ describe('InternalNotificationController - Send case files updated notifications it('should send notifications', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: spokespersonName, address: spokespersonEmail }], + to: [{ name: spokesperson.name, address: spokesperson.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -127,21 +133,21 @@ describe('InternalNotificationController - Send case files updated notifications it('should send notifications', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: spokespersonName, address: spokespersonEmail }], + to: [{ name: spokesperson.name, address: spokesperson.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCourtDateNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCourtDateNotifications.spec.ts index 18bf5d712b71..6cb820f993dc 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCourtDateNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCourtDateNotifications.spec.ts @@ -6,10 +6,14 @@ import { CaseNotificationType, CaseType, DateType, + NotificationType, User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' @@ -31,13 +35,12 @@ type GivenWhenThen = ( describe('InternalNotificationController - Send court date notifications', () => { const userId = uuid() const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const defenderName = uuid() - const defenderEmail = uuid() + const courtName = 'Héraðsdómur Reykjavíkur' const courtCaseNumber = uuid() + const { prosecutor, defender } = createTestUsers(['prosecutor', 'defender']) + let mockEmailService: EmailService let mockNotificationModel: typeof Notification let givenWhenThen: GivenWhenThen @@ -75,11 +78,11 @@ describe('InternalNotificationController - Send court date notifications', () => const theCase = { id: caseId, type: CaseType.CUSTODY, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, court: { name: courtName }, courtCaseNumber, - defenderName, - defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, } as Case beforeEach(async () => { @@ -89,23 +92,23 @@ describe('InternalNotificationController - Send court date notifications', () => it('should send notifications to prosecutor and defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Fyrirtaka í máli: ${courtCaseNumber}`, - html: `Héraðsdómur Reykjavíkur hefur staðfest fyrirtökutíma fyrir kröfu um gæsluvarðhald.

Fyrirtaka mun fara fram á ótilgreindum tíma.

Dómsalur hefur ekki verið skráður.

Dómari hefur ekki verið skráður.

Verjandi sakbornings: ${defenderName}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, + html: `Héraðsdómur Reykjavíkur hefur staðfest fyrirtökutíma fyrir kröfu um gæsluvarðhald.

Fyrirtaka mun fara fram á ótilgreindum tíma.

Dómsalur hefur ekki verið skráður.

Dómari hefur ekki verið skráður.

Verjandi sakbornings: ${defender.name}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Fyrirtaka í máli ${courtCaseNumber}`, - html: `Héraðsdómur Reykjavíkur hefur boðað þig í fyrirtöku sem verjanda sakbornings.

Fyrirtaka mun fara fram á ótilgreindum tíma.

Málsnúmer: ${courtCaseNumber}.

Dómsalur hefur ekki verið skráður.

Dómari: .

Sækjandi: ${prosecutorName} ().`, + html: `Héraðsdómur Reykjavíkur hefur boðað þig í fyrirtöku sem verjanda sakbornings.

Fyrirtaka mun fara fram á ótilgreindum tíma.

Málsnúmer: ${courtCaseNumber}.

Dómsalur hefur ekki verið skráður.

Dómari: .

Sækjandi: ${prosecutor.name} ().`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Yfirlit máls ${courtCaseNumber}`, }), ) @@ -125,11 +128,11 @@ describe('InternalNotificationController - Send court date notifications', () => const theCase = { id: caseId, type: CaseType.CUSTODY, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, court: { name: courtName }, courtCaseNumber, - defenderName, - defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, } as Case beforeEach(async () => { @@ -142,8 +145,8 @@ describe('InternalNotificationController - Send court date notifications', () => notifications: [ { caseId, - type: CaseNotificationType.READY_FOR_COURT, - recipients: [{ address: defenderEmail, success: true }], + type: NotificationType.READY_FOR_COURT, + recipients: [{ address: defender.email, success: true }], }, ], } as Case, @@ -154,7 +157,7 @@ describe('InternalNotificationController - Send court date notifications', () => it('should not send link to case to defender', () => { expect(mockEmailService.sendEmail).not.toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Yfirlit máls ${courtCaseNumber}`, }), ) @@ -175,11 +178,11 @@ describe('InternalNotificationController - Send court date notifications', () => const theCase = { id: caseId, type: CaseType.INDICTMENT, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, defendants: [ { - defenderName, - defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, }, ], court: { name: courtName }, @@ -194,7 +197,7 @@ describe('InternalNotificationController - Send court date notifications', () => it('should send notifications to prosecutor and defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Nýtt þinghald í máli ${courtCaseNumber}`, html: `Héraðsdómur Reykjavíkur boðar til þinghalds í máli ${courtCaseNumber}.
Fyrirtaka mun fara fram 2. maí 2024, kl. 14:32.

Tegund þinghalds: Óþekkt.

Dómsalur hefur ekki verið skráður.

Dómari hefur ekki verið skráður.

Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -202,7 +205,7 @@ describe('InternalNotificationController - Send court date notifications', () => expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Nýtt þinghald í máli ${courtCaseNumber}`, html: `Héraðsdómur Reykjavíkur boðar til þinghalds í máli ${courtCaseNumber}.
Fyrirtaka mun fara fram 2. maí 2024, kl. 14:32.

Tegund þinghalds: Óþekkt.

Dómsalur hefur ekki verið skráður.

Dómari hefur ekki verið skráður.

Hægt er að nálgast gögn málsins hjá Héraðsdómur Reykjavíkur.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendDefendantsNotUpdatedAtCourtNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendDefendantsNotUpdatedAtCourtNotifications.spec.ts index b7090ce986dc..b0448904d07f 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendDefendantsNotUpdatedAtCourtNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendDefendantsNotUpdatedAtCourtNotifications.spec.ts @@ -2,9 +2,16 @@ import { uuid } from 'uuidv4' import { EmailService } from '@island.is/email-service' -import { CaseNotificationType, User } from '@island.is/judicial-system/types' +import { + CaseNotificationType, + NotificationType, + User, +} from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' @@ -27,14 +34,15 @@ describe('InternalNotificationController - Send defendants not updated at court user: { id: userId } as User, type: CaseNotificationType.DEFENDANTS_NOT_UPDATED_AT_COURT, } + + const { registrar } = createTestUsers(['registrar', 'defender']) const caseId = uuid() const courtCaseNumber = uuid() - const registrarName = uuid() - const registrarEmail = uuid() + const theCase = { id: caseId, courtCaseNumber, - registrar: { name: registrarName, email: registrarEmail }, + registrar: { name: registrar.name, email: registrar.email }, } as Case let mockEmailService: EmailService @@ -72,7 +80,7 @@ describe('InternalNotificationController - Send defendants not updated at court it('should send email', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: registrarName, address: registrarEmail }], + to: [{ name: registrar.name, address: registrar.email }], subject: `Skráning varnaraðila/verjenda í máli ${courtCaseNumber}`, html: `Ekki tókst að skrá varnaraðila/verjendur í máli ${courtCaseNumber} í Auði. Yfirfara þarf málið í Auði og skrá rétta aðila áður en því er lokað.`, }), @@ -91,8 +99,8 @@ describe('InternalNotificationController - Send defendants not updated at court ...theCase, notifications: [ { - type: CaseNotificationType.DEFENDANTS_NOT_UPDATED_AT_COURT, - recipients: [{ address: registrarEmail, success: true }], + type: NotificationType.DEFENDANTS_NOT_UPDATED_AT_COURT, + recipients: [{ address: registrar.email, success: true }], }, ], } as Case, diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentDeniedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentDeniedNotifications.spec.ts index 904b873650f9..7efd487f6146 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentDeniedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentDeniedNotifications.spec.ts @@ -8,7 +8,10 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' @@ -29,8 +32,10 @@ type GivenWhenThen = ( describe('InternalNotificationController - Send indictment denied notification', () => { const userId = uuid() const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() + + const { prosecutor } = createTestUsers(['prosecutor']) + const prosecutorName = prosecutor.name + const prosecutorEmail = prosecutor.email const policeCaseNumbers = [uuid(), uuid()] let mockEmailService: EmailService diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentReturnedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentReturnedNotifications.spec.ts index 24a9ddba4558..ee1f6bd106bb 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentReturnedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentReturnedNotifications.spec.ts @@ -8,7 +8,10 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' @@ -27,10 +30,10 @@ type GivenWhenThen = ( ) => Promise describe('InternalNotificationController - Send indictment returned notification', () => { + const { prosecutor } = createTestUsers(['prosecutor']) const userId = uuid() const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() + const policeCaseNumbers = [uuid(), uuid()] const courtName = uuid() @@ -69,7 +72,7 @@ describe('InternalNotificationController - Send indictment returned notification const theCase = { id: caseId, type: CaseType.INDICTMENT, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, policeCaseNumbers, court: { name: courtName }, } as Case @@ -81,7 +84,7 @@ describe('InternalNotificationController - Send indictment returned notification it('should send notifications to prosecutor', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Ákæra endursend í máli ${policeCaseNumbers[0]}`, html: `${courtName} hefur endursent ákæru vegna lögreglumáls ${policeCaseNumbers[0]}. Þú getur nálgast samantekt málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts index d3992015ded4..3c20d0d96a26 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts @@ -4,7 +4,10 @@ import { EmailService } from '@island.is/email-service' import { InstitutionNotificationType } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { InternalCaseService } from '../../../case' import { UserService } from '../../../user' @@ -18,10 +21,11 @@ interface Then { type GivenWhenThen = () => Promise describe('InternalNotificationController - Send indictments waiting for confirmation notifications', () => { - const prosecutorName1 = uuid() - const prosecutorEmail1 = uuid() - const prosecutorName2 = uuid() - const prosecutorEmail2 = uuid() + const { prosecutor1, prosecutor2 } = createTestUsers([ + 'prosecutor1', + 'prosecutor2', + ]) + const prosecutorsOfficeId = uuid() let mockUserService: UserService let mockInternalCaseService: InternalCaseService @@ -116,25 +120,25 @@ describe('InternalNotificationController - Send indictments waiting for confirma const mockGetUsersWhoCanConfirmIndictments = mockUserService.getUsersWhoCanConfirmIndictments as jest.Mock mockGetUsersWhoCanConfirmIndictments.mockResolvedValueOnce([ - { name: prosecutorName1, email: prosecutorEmail1 }, - { name: prosecutorName2, email: prosecutorEmail2 }, + { name: prosecutor1.name, email: prosecutor1.email }, + { name: prosecutor2.name, email: prosecutor2.email }, ]) then = await givenWhenThen() }) - it('should not send messages', () => { + it('should send messages', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(2) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName1, address: prosecutorEmail1 }], + to: [{ name: prosecutor1.name, address: prosecutor1.email }], subject: 'Ákærur bíða staðfestingar', html: 'Í Réttarvörslugátt bíða 2 ákærur staðfestingar.

Hægt er að nálgast yfirlit og staðfesta ákærur í Réttarvörslugátt.', }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName2, address: prosecutorEmail2 }], + to: [{ name: prosecutor2.name, address: prosecutor2.email }], subject: 'Ákærur bíða staðfestingar', html: 'Í Réttarvörslugátt bíða 2 ákærur staðfestingar.

Hægt er að nálgast yfirlit og staðfesta ákærur í Réttarvörslugátt.', }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendReadyForCourtNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendReadyForCourtNotifications.spec.ts index e1b3b2c1a4c6..f9757746c4ad 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendReadyForCourtNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendReadyForCourtNotifications.spec.ts @@ -15,11 +15,15 @@ import { CaseType, DateType, IndictmentSubtype, + NotificationType, RequestSharedWithDefender, User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { randomDate } from '../../../../test' import { Case } from '../../../case' @@ -44,23 +48,29 @@ describe('InternalNotificationController - Send ready for court notifications fo const userId = uuid() const caseId = uuid() const policeCaseNumber = uuid() - const courtId = uuid() const courtCaseNumber = uuid() + + const { prosecutor, defender, testCourt } = createTestUsers([ + 'prosecutor', + 'defender', + 'testCourt', + ]) + const theCase = { id: caseId, type: CaseType.CUSTODY, state: CaseState.RECEIVED, policeCaseNumbers: [policeCaseNumber], prosecutor: { - name: 'Derrick', - email: 'derrick@dummy.is', + name: prosecutor.name, + email: prosecutor.email, }, - courtId, + courtId: testCourt.id, court: { name: 'Héraðsdómur Reykjavíkur' }, courtCaseNumber, - defenderNationalId: uuid(), - defenderName: 'Saul Goodman', - defenderEmail: 'saul@dummy.is', + defenderNationalId: defender.nationalId, + defenderName: defender.name, + defenderEmail: defender.email, requestSharedWithDefender: RequestSharedWithDefender.COURT_DATE, prosecutorsOffice: { name: 'Héraðsdómur Derricks' }, dateLogs: [{ date: randomDate(), dateType: DateType.ARRAIGNMENT_DATE }], @@ -69,7 +79,6 @@ describe('InternalNotificationController - Send ready for court notifications fo user: { id: userId } as User, type: CaseNotificationType.READY_FOR_COURT, } - const courtMobileNumber = uuid() let mockEmailService: EmailService let mockSmsService: SmsService @@ -77,7 +86,7 @@ describe('InternalNotificationController - Send ready for court notifications fo let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.COURTS_MOBILE_NUMBERS = `{"${courtId}": "${courtMobileNumber}"}` + process.env.COURTS_MOBILE_NUMBERS = `{"${testCourt.id}": "${testCourt.mobile}"}` const { emailService, @@ -119,7 +128,7 @@ describe('InternalNotificationController - Send ready for court notifications fo name: mockNotificationConfig.email.replyToName, address: mockNotificationConfig.email.replyToEmail, }, - to: [{ name: 'Derrick', address: 'derrick@dummy.is' }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: 'Krafa um gæsluvarðhald send', text: `Þú hefur sent kröfu á Héraðsdóm Reykjavíkur vegna LÖKE máls ${policeCaseNumber}. Skjalið er aðgengilegt undir málinu í Réttarvörslugátt.`, html: `Þú hefur sent kröfu á Héraðsdóm Reykjavíkur vegna LÖKE máls ${policeCaseNumber}. Skjalið er aðgengilegt undir málinu í Réttarvörslugátt.`, @@ -129,8 +138,8 @@ describe('InternalNotificationController - Send ready for court notifications fo it('should send ready for court sms notification to court', () => { expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [courtMobileNumber], - 'Gæsluvarðhaldskrafa tilbúin til afgreiðslu. Sækjandi: Derrick (Héraðsdómur Derricks). Sjá nánar á rettarvorslugatt.island.is.', + [testCourt.mobile], + `Gæsluvarðhaldskrafa tilbúin til afgreiðslu. Sækjandi: ${prosecutor.name} (Héraðsdómur Derricks). Sjá nánar á rettarvorslugatt.island.is.`, ) }) @@ -148,11 +157,13 @@ describe('InternalNotificationController - Send ready for court notifications fo notifications: [ { caseId, - type: CaseNotificationType.READY_FOR_COURT, + type: NotificationType.READY_FOR_COURT, recipients: [ { address: - mockNotificationConfig.sms.courtsMobileNumbers[courtId], + mockNotificationConfig.sms.courtsMobileNumbers[ + testCourt.id + ], success: true, }, ], @@ -173,7 +184,7 @@ describe('InternalNotificationController - Send ready for court notifications fo name: mockNotificationConfig.email.replyToName, address: mockNotificationConfig.email.replyToEmail, }, - to: [{ name: 'Derrick', address: 'derrick@dummy.is' }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: 'Krafa um gæsluvarðhald send', text: `Þú hefur sent kröfu á Héraðsdóm Reykjavíkur vegna LÖKE máls ${policeCaseNumber}. Skjalið er aðgengilegt undir málinu í Réttarvörslugátt.`, html: `Þú hefur sent kröfu á Héraðsdóm Reykjavíkur vegna LÖKE máls ${policeCaseNumber}. Skjalið er aðgengilegt undir málinu í Réttarvörslugátt.`, @@ -183,7 +194,7 @@ describe('InternalNotificationController - Send ready for court notifications fo it('should send ready for court sms notification to court', () => { expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [courtMobileNumber], + [testCourt.mobile], `Sækjandi í máli ${courtCaseNumber} hefur breytt kröfunni og sent aftur á héraðsdómstól. Nýtt kröfuskjal hefur verið vistað í Auði. Sjá nánar á rettarvorslugatt.island.is.`, ) }) @@ -191,7 +202,7 @@ describe('InternalNotificationController - Send ready for court notifications fo it('should not send ready for court email notification to defender', () => { expect(mockEmailService.sendEmail).not.toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: 'Saul Goodman', address: 'saul@dummy.is' }], + to: [{ name: defender.name, address: defender.email }], }), ) }) @@ -224,8 +235,8 @@ describe('InternalNotificationController - Send ready for court notifications fo ...theCase, notifications: [ { - type: CaseNotificationType.READY_FOR_COURT, - recipients: [{ address: 'saul@dummy.is', success: true }], + type: NotificationType.READY_FOR_COURT, + recipients: [{ address: defender.email, success: true }], }, ], } as Case, @@ -245,7 +256,7 @@ describe('InternalNotificationController - Send ready for court notifications fo name: mockNotificationConfig.email.replyToName, address: mockNotificationConfig.email.replyToEmail, }, - to: [{ name: 'Saul Goodman', address: 'saul@dummy.is' }], + to: [{ name: defender.name, address: defender.email }], subject: `Krafa í máli ${courtCaseNumber}`, html: `Sækjandi í máli ${courtCaseNumber} hjá Héraðsdómi Reykjavíkur hefur breytt kröfunni og sent hana aftur á dóminn.

Þú getur nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, attachments: undefined, @@ -257,8 +268,8 @@ describe('InternalNotificationController - Send ready for court notifications fo describe('InternalNotificationController - Send ready for court notifications for indictment cases', () => { const userId = uuid() - const courtId = uuid() - const courtEmail = uuid() + const { testCourt } = createTestUsers(['testCourt']) + const notificationDto = { user: { id: userId } as User, type: CaseNotificationType.READY_FOR_COURT, @@ -270,7 +281,7 @@ describe('InternalNotificationController - Send ready for court notifications fo let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.COURTS_EMAILS = `{"${courtId}": "${courtEmail}"}` + process.env.COURTS_EMAILS = `{"${testCourt.id}": "${testCourt.email}"}` const { emailService, @@ -298,8 +309,9 @@ describe('InternalNotificationController - Send ready for court notifications fo describe('indictment notification with single indictment subtype', () => { const caseId = uuid() const policeCaseNumbers = [uuid()] + const court = { - id: courtId, + id: testCourt.id, name: 'Héraðsdómur Reykjavíkur', } as Institution const prosecutorsOffice = { name: 'Lögreglan á höfuðborgarsvæðinu' } @@ -312,7 +324,7 @@ describe('InternalNotificationController - Send ready for court notifications fo indictmentSubtypes: { [policeCaseNumbers[0]]: [IndictmentSubtype.MURDER], }, - courtId, + courtId: court.id, court, prosecutorsOffice, } as unknown as Case @@ -327,7 +339,7 @@ describe('InternalNotificationController - Send ready for court notifications fo to: [ { name: 'Héraðsdómur Reykjavíkur', - address: courtEmail, + address: testCourt.email, }, ], subject: 'Ákæra tilbúin til afgreiðslu', @@ -337,7 +349,9 @@ describe('InternalNotificationController - Send ready for court notifications fo expect(mockNotificationModel.create).toHaveBeenCalledWith({ caseId, type: CaseNotificationType.READY_FOR_COURT, - recipients: [{ success: true, address: courtEmail }] as Recipient[], + recipients: [ + { success: true, address: testCourt.email }, + ] as Recipient[], }) }) }) @@ -346,7 +360,7 @@ describe('InternalNotificationController - Send ready for court notifications fo const caseId = uuid() const policeCaseNumbers = [uuid(), uuid()] const court = { - id: courtId, + id: testCourt.id, name: 'Héraðsdómur Reykjavíkur', } as Institution const prosecutorsOffice = { name: 'Lögreglan á höfuðborgarsvæðinu' } @@ -366,7 +380,7 @@ describe('InternalNotificationController - Send ready for court notifications fo IndictmentSubtype.THEFT, ], }, - courtId, + courtId: court.id, court, prosecutorsOffice, } as unknown as Case @@ -381,7 +395,7 @@ describe('InternalNotificationController - Send ready for court notifications fo to: [ { name: 'Héraðsdómur Reykjavíkur', - address: courtEmail, + address: testCourt.email, }, ], subject: 'Ákæra tilbúin til afgreiðslu', @@ -391,7 +405,9 @@ describe('InternalNotificationController - Send ready for court notifications fo expect(mockNotificationModel.create).toHaveBeenCalledWith({ caseId, type: CaseNotificationType.READY_FOR_COURT, - recipients: [{ success: true, address: courtEmail }] as Recipient[], + recipients: [ + { success: true, address: testCourt.email }, + ] as Recipient[], }) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRevokedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRevokedNotifications.spec.ts index c585586d5c1d..274a52a42c74 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRevokedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRevokedNotifications.spec.ts @@ -2,9 +2,15 @@ import { uuid } from 'uuidv4' import { EmailService } from '@island.is/email-service' -import { CaseNotificationType } from '@island.is/judicial-system/types' +import { + CaseNotificationType, + NotificationType, +} from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' @@ -19,22 +25,27 @@ interface Then { type GivenWhenThen = (notifications?: Notification[]) => Promise describe('InternalNotificationController - Send revoked notifications for indictment cases', () => { + const { judge, registrar, defender } = createTestUsers([ + 'judge', + 'registrar', + 'defender', + ]) const caseId = uuid() - const judgeName = uuid() - const judgeEmail = uuid() - const registrarName = uuid() - const registrarEmail = uuid() - const defenderNationalId = uuid() - const defenderName = uuid() - const defenderEmail = uuid() + const prosecutorsOfficeName = uuid() const courtName = uuid() const courtCaseNumber = uuid() const theCase = { id: caseId, - judge: { name: judgeName, email: judgeEmail }, - registrar: { name: registrarName, email: registrarEmail }, - defendants: [{ defenderNationalId, defenderName, defenderEmail }], + judge: { name: judge.name, email: judge.email }, + registrar: { name: registrar.name, email: registrar.email }, + defendants: [ + { + defenderNationalId: defender.nationalId, + defenderName: defender.name, + defenderEmail: defender.email, + }, + ], creatingProsecutor: { institution: { name: prosecutorsOfficeName } }, court: { name: courtName }, courtCaseNumber, @@ -73,8 +84,8 @@ describe('InternalNotificationController - Send revoked notifications for indict beforeEach(async () => { then = await givenWhenThen([ { - type: CaseNotificationType.COURT_DATE, - recipients: [{ address: defenderEmail, success: true }], + type: NotificationType.COURT_DATE, + recipients: [{ address: defender.email, success: true }], } as Notification, ]) }) @@ -82,21 +93,21 @@ describe('InternalNotificationController - Send revoked notifications for indict it('should send a notifications', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ address: judgeEmail, name: judgeName }], + to: [{ address: judge.email, name: judge.name }], subject: `Ákæra afturkölluð í máli ${courtCaseNumber}`, html: `${prosecutorsOfficeName} hefur afturkallað ákæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ address: registrarEmail, name: registrarName }], + to: [{ address: registrar.email, name: registrar.name }], subject: `Ákæra afturkölluð í máli ${courtCaseNumber}`, html: `${prosecutorsOfficeName} hefur afturkallað ákæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ address: defenderEmail, name: defenderName }], + to: [{ address: defender.email, name: defender.name }], subject: `Ákæra afturkölluð í máli ${courtCaseNumber}`, html: `Dómstóllinn hafði skráð þig sem verjanda í málinu.

Sjá nánar á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -105,9 +116,9 @@ describe('InternalNotificationController - Send revoked notifications for indict caseId: caseId, type: CaseNotificationType.REVOKED, recipients: [ - { address: judgeEmail, success: true }, - { address: registrarEmail, success: true }, - { address: defenderEmail, success: true }, + { address: judge.email, success: true }, + { address: registrar.email, success: true }, + { address: defender.email, success: true }, ], }) expect(then.result).toEqual({ delivered: true }) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts index 26ccba06fa36..3effe645d714 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts @@ -16,7 +16,10 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { Defendant, DefendantService } from '../../../defendant' @@ -43,6 +46,7 @@ describe('InternalNotificationController - Send ruling notifications', () => { user: { id: userId } as User, type: CaseNotificationType.RULING, } + const { testProsecutor } = createTestUsers(['testProsecutor']) let mockEmailService: EmailService let mockConfig: ConfigType @@ -50,7 +54,8 @@ describe('InternalNotificationController - Send ruling notifications', () => { let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.PRISON_EMAIL = 'prisonEmail@email.com,prisonEmail2@email.com' + process.env.PRISON_EMAIL = + 'prisonEmail@omnitrix.is,prisonEmail2@omnitrix.is' const { emailService, @@ -82,7 +87,11 @@ describe('InternalNotificationController - Send ruling notifications', () => { describe('email to prosecutor for indictment case', () => { const caseId = uuid() - const prosecutor = { name: 'Lögmaður', email: 'logmadur@gmail.com' } + + const prosecutor = { + name: testProsecutor.name, + email: testProsecutor.email, + } const theCase = { id: caseId, type: CaseType.INDICTMENT, @@ -111,7 +120,10 @@ describe('InternalNotificationController - Send ruling notifications', () => { describe('email to prosecutor for restriction case', () => { const caseId = uuid() - const prosecutor = { name: 'Lögmaður', email: 'logmadur@gmail.com' } + const prosecutor = { + name: testProsecutor.name, + email: testProsecutor.email, + } const theCase = { id: caseId, state: CaseState.ACCEPTED, @@ -128,8 +140,7 @@ describe('InternalNotificationController - Send ruling notifications', () => { it('should send email to prosecutor', () => { const expectedLink = `` expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(2) - expect(mockEmailService.sendEmail).toHaveBeenNthCalledWith( - 1, + expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ to: [{ name: prosecutor.name, address: prosecutor.email }], subject: 'Úrskurður í máli 007-2022-07', @@ -141,7 +152,10 @@ describe('InternalNotificationController - Send ruling notifications', () => { describe('email to prosecutor for modified ruling restriction case', () => { const caseId = uuid() - const prosecutor = { name: 'Lögmaður', email: 'logmadur@gmail.com' } + const prosecutor = { + name: testProsecutor.name, + email: testProsecutor.email, + } const theCase = { id: caseId, type: CaseType.CUSTODY, @@ -159,10 +173,9 @@ describe('InternalNotificationController - Send ruling notifications', () => { it('should send email to prosecutor', () => { const expectedLink = `` expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(2) - expect(mockEmailService.sendEmail).toHaveBeenNthCalledWith( - 1, + expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutor.name, address: prosecutor.email }], + to: [{ name: testProsecutor.name, address: testProsecutor.email }], subject: 'Úrskurður í máli 007-2022-07 leiðréttur', html: `Dómari hefur leiðrétt úrskurð í máli 007-2022-07 hjá Héraðsdómi Reykjavíkur.

Skjöl málsins eru aðgengileg á ${expectedLink}yfirlitssíðu málsins í Réttarvörslugátt
.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts b/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts index 80603c97865d..2394ff1249f2 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts @@ -1,9 +1,18 @@ -import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator' +import { Transform } from 'class-transformer' +import { + IsEnum, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' import { DefenderChoice, ServiceStatus } from '@island.is/judicial-system/types' +import { nationalIdTransformer } from '../../../transformers' + export class UpdateSubpoenaDto { @IsOptional() @IsEnum(ServiceStatus) @@ -12,6 +21,7 @@ export class UpdateSubpoenaDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly servedBy?: string @@ -32,21 +42,27 @@ export class UpdateSubpoenaDto { @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderName?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderEmail?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly defenderPhoneNumber?: string @@ -57,16 +73,15 @@ export class UpdateSubpoenaDto { @IsOptional() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiPropertyOptional({ type: String }) readonly requestedDefenderNationalId?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly requestedDefenderName?: string - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean }) - readonly isDefenderChoiceConfirmed?: boolean } diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts b/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts index 411c8f3e4ef3..968440871a92 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts @@ -33,7 +33,7 @@ import { DeliverResponse } from './models/deliver.response' import { Subpoena } from './models/subpoena.model' import { SubpoenaService } from './subpoena.service' -@Controller('api/internal/') +@Controller('api/internal') @ApiTags('internal subpoenas') @UseGuards(TokenGuard) export class InternalSubpoenaController { diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.controller.ts b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.controller.ts index da84d9c66c59..da139a356906 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.controller.ts @@ -5,7 +5,6 @@ import { Get, Header, Inject, - InternalServerErrorException, Param, Query, Res, diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts index 57b9d55af2be..31fba1e49734 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts @@ -4,6 +4,7 @@ import { SequelizeModule } from '@nestjs/sequelize' import { MessageModule } from '@island.is/judicial-system/message' import { CaseModule } from '../case/case.module' +import { DefendantModule } from '../defendant/defendant.module' import { Defendant } from '../defendant/models/defendant.model' import { EventModule } from '../event/event.module' import { FileModule } from '../file/file.module' @@ -21,7 +22,7 @@ import { SubpoenaService } from './subpoena.service' forwardRef(() => FileModule), forwardRef(() => MessageModule), forwardRef(() => EventModule), - + forwardRef(() => DefendantModule), SequelizeModule.forFeature([Subpoena, Defendant]), ], controllers: [ diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts index 9f7322f11fe3..5c9bea446004 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts @@ -1,5 +1,5 @@ import { Base64 } from 'js-base64' -import { Includeable, Sequelize } from 'sequelize' +import { Includeable } from 'sequelize' import { Transaction } from 'sequelize/types' import { @@ -8,7 +8,7 @@ import { Injectable, InternalServerErrorException, } from '@nestjs/common' -import { InjectConnection, InjectModel } from '@nestjs/sequelize' +import { InjectModel } from '@nestjs/sequelize' import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' @@ -20,7 +20,6 @@ import { } from '@island.is/judicial-system/message' import { CaseFileCategory, - DefenderChoice, isFailedServiceStatus, isSuccessfulServiceStatus, isTrafficViolationCase, @@ -31,6 +30,7 @@ import { import { Case } from '../case/models/case.model' import { PdfService } from '../case/pdf.service' +import { DefendantService } from '../defendant/defendant.service' import { Defendant } from '../defendant/models/defendant.model' import { EventService } from '../event' import { FileService } from '../file' @@ -61,9 +61,7 @@ export const include: Includeable[] = [ @Injectable() export class SubpoenaService { constructor( - @InjectConnection() private readonly sequelize: Sequelize, @InjectModel(Subpoena) private readonly subpoenaModel: typeof Subpoena, - @InjectModel(Defendant) private readonly defendantModel: typeof Defendant, private readonly pdfService: PdfService, private readonly messageService: MessageService, @Inject(forwardRef(() => PoliceService)) @@ -71,6 +69,7 @@ export class SubpoenaService { @Inject(forwardRef(() => FileService)) private readonly fileService: FileService, private readonly eventService: EventService, + private readonly defendantService: DefendantService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -111,53 +110,36 @@ export class SubpoenaService { private async addMessagesForSubpoenaUpdateToQueue( subpoena: Subpoena, serviceStatus?: ServiceStatus, - defenderChoice?: DefenderChoice, - defenderNationalId?: string, ): Promise { - const messages: Message[] = [] + let message: Message | undefined = undefined if (serviceStatus && serviceStatus !== subpoena.serviceStatus) { if (isSuccessfulServiceStatus(serviceStatus)) { - messages.push({ + message = { type: MessageType.SUBPOENA_NOTIFICATION, caseId: subpoena.caseId, elementId: [subpoena.defendantId, subpoena.id], body: { type: SubpoenaNotificationType.SERVICE_SUCCESSFUL, }, - }) + } } else if (isFailedServiceStatus(serviceStatus)) { - messages.push({ + message = { type: MessageType.SUBPOENA_NOTIFICATION, caseId: subpoena.caseId, elementId: [subpoena.defendantId, subpoena.id], body: { type: SubpoenaNotificationType.SERVICE_FAILED, }, - }) + } } } - if ( - defenderChoice === DefenderChoice.CHOOSE && - (defenderChoice !== subpoena.defendant?.defenderChoice || - defenderNationalId !== subpoena.defendant?.defenderNationalId) - ) { - messages.push({ - type: MessageType.SUBPOENA_NOTIFICATION, - caseId: subpoena.caseId, - elementId: [subpoena.defendantId, subpoena.id], - body: { - type: SubpoenaNotificationType.DEFENDANT_SELECTED_DEFENDER, - }, - }) - } - - if (messages.length === 0) { + if (!message) { return } - return this.messageService.sendMessagesToQueue(messages) + return this.messageService.sendMessagesToQueue([message]) } async update( @@ -191,57 +173,53 @@ export class SubpoenaService { } if ( - defenderChoice || - defenderNationalId || - requestedDefenderChoice || - requestedDefenderNationalId + subpoena.case && + subpoena.defendant && + (defenderChoice || + defenderNationalId || + defenderName || + defenderEmail || + defenderPhoneNumber || + requestedDefenderChoice || + requestedDefenderNationalId || + requestedDefenderName) ) { // If there is a change in the defender choice after the judge has confirmed the choice, // we need to set the isDefenderChoiceConfirmed to false - const isChangingDefenderChoice = - (defenderChoice && + const resetDefenderChoiceConfirmed = + subpoena.defendant?.isDefenderChoiceConfirmed && + ((defenderChoice && subpoena.defendant?.defenderChoice !== defenderChoice) || - (defenderNationalId && - subpoena.defendant?.defenderNationalId !== defenderNationalId && - subpoena.defendant?.isDefenderChoiceConfirmed) - - const defendantUpdate: Partial = { - defenderChoice, - defenderNationalId, - defenderName, - defenderEmail, - defenderPhoneNumber, - requestedDefenderChoice, - requestedDefenderNationalId, - requestedDefenderName, - isDefenderChoiceConfirmed: isChangingDefenderChoice ? false : undefined, - } + (defenderNationalId && + subpoena.defendant?.defenderNationalId !== defenderNationalId)) - const [numberOfAffectedRows] = await this.defendantModel.update( - defendantUpdate, + // Færa message handling í defendant service + await this.defendantService.updateRestricted( + subpoena.case, + subpoena.defendant, { - where: { id: subpoena.defendantId }, - transaction, + defenderChoice, + defenderNationalId, + defenderName, + defenderEmail, + defenderPhoneNumber, + requestedDefenderChoice, + requestedDefenderNationalId, + requestedDefenderName, }, + resetDefenderChoiceConfirmed ? false : undefined, + transaction, ) - - if (numberOfAffectedRows > 1) { - // Tolerate failure, but log error - this.logger.error( - `Unexpected number of rows ${numberOfAffectedRows} affected when updating defendant`, - ) - } } // No need to wait for this to finish - this.addMessagesForSubpoenaUpdateToQueue( - subpoena, - serviceStatus, - defenderChoice, - defenderNationalId, - ) + this.addMessagesForSubpoenaUpdateToQueue(subpoena, serviceStatus) - if (update.serviceStatus && subpoena.case) { + if ( + update.serviceStatus && + update.serviceStatus !== subpoena.serviceStatus && + subpoena.case + ) { this.eventService.postEvent( 'SUBPOENA_SERVICE_STATUS', subpoena.case, diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/createTestingSubpoenaModule.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/createTestingSubpoenaModule.ts index 4a7f93290f45..e397c3e498ac 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/test/createTestingSubpoenaModule.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/createTestingSubpoenaModule.ts @@ -13,7 +13,7 @@ import { import { MessageService } from '@island.is/judicial-system/message' import { CaseService, PdfService } from '../../case' -import { Defendant } from '../../defendant' +import { Defendant, DefendantService } from '../../defendant' import { EventService } from '../../event' import { FileService } from '../../file' import { PoliceService } from '../../police' @@ -30,6 +30,7 @@ jest.mock('../../case/pdf.service') jest.mock('../../police/police.service') jest.mock('../../file/file.service') jest.mock('../../event/event.service') +jest.mock('../../defendant/defendant.service') jest.mock('@island.is/judicial-system/message') export const createTestingSubpoenaModule = async () => { @@ -49,6 +50,7 @@ export const createTestingSubpoenaModule = async () => { PoliceService, FileService, EventService, + DefendantService, { provide: LOGGER_PROVIDER, useValue: { diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts index c2f9ff68a082..43255bd30aa1 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts @@ -4,6 +4,7 @@ import { createTestingSubpoenaModule } from '../createTestingSubpoenaModule' import { Case } from '../../../case' import { Defendant } from '../../../defendant' +import { DeliverDto } from '../../dto/deliver.dto' import { DeliverResponse } from '../../models/deliver.response' import { Subpoena } from '../../models/subpoena.model' import { SubpoenaService } from '../../subpoena.service' @@ -22,7 +23,7 @@ describe('InternalSubpoenaController - Deliver subpoena to police', () => { const subpoena = { id: subpoenaId } as Subpoena const defendant = { id: defendantId, subpoenas: [subpoena] } as Defendant const theCase = { id: caseId } as Case - const user = { user: { id: uuid() } } as any + const user = { user: { id: uuid() } } as DeliverDto const delivered = { delivered: true } as DeliverResponse let mockSubpoenaService: SubpoenaService diff --git a/apps/judicial-system/backend/src/app/modules/user/dto/createUser.dto.ts b/apps/judicial-system/backend/src/app/modules/user/dto/createUser.dto.ts index 1460431b37f7..b6ec9c4cfbc6 100644 --- a/apps/judicial-system/backend/src/app/modules/user/dto/createUser.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/user/dto/createUser.dto.ts @@ -1,38 +1,50 @@ +import { Transform } from 'class-transformer' import { IsBoolean, IsEnum, IsNotEmpty, IsString, IsUUID, + MaxLength, + MinLength, } from 'class-validator' import { ApiProperty } from '@nestjs/swagger' import { UserRole } from '@island.is/judicial-system/types' +import { nationalIdTransformer } from '../../../transformers' + export class CreateUserDto { @IsNotEmpty() @IsString() + @MinLength(10) + @MaxLength(10) + @Transform(nationalIdTransformer) @ApiProperty({ type: String }) readonly nationalId!: string @IsNotEmpty() @IsString() + @MaxLength(255) @ApiProperty({ type: String }) readonly name!: string @IsNotEmpty() @IsString() + @MaxLength(255) @ApiProperty({ type: String }) readonly title!: string @IsNotEmpty() @IsString() + @MaxLength(255) @ApiProperty({ type: String }) readonly mobileNumber!: string @IsNotEmpty() @IsString() + @MaxLength(255) @ApiProperty({ type: String }) readonly email!: string diff --git a/apps/judicial-system/backend/src/app/modules/user/dto/updateUser.dto.ts b/apps/judicial-system/backend/src/app/modules/user/dto/updateUser.dto.ts index fcebedbf7405..b97390a4a3ac 100644 --- a/apps/judicial-system/backend/src/app/modules/user/dto/updateUser.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/user/dto/updateUser.dto.ts @@ -4,6 +4,7 @@ import { IsOptional, IsString, IsUUID, + MaxLength, } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' @@ -13,21 +14,25 @@ import { UserRole } from '@island.is/judicial-system/types' export class UpdateUserDto { @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly name?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly title?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly mobileNumber?: string @IsOptional() @IsString() + @MaxLength(255) @ApiPropertyOptional({ type: String }) readonly email?: string diff --git a/apps/judicial-system/backend/src/app/transformers/index.ts b/apps/judicial-system/backend/src/app/transformers/index.ts new file mode 100644 index 000000000000..fa1e56c76c61 --- /dev/null +++ b/apps/judicial-system/backend/src/app/transformers/index.ts @@ -0,0 +1 @@ +export { nationalIdTransformer } from './nationalId.transformer' diff --git a/apps/judicial-system/backend/src/app/transformers/nationalId.transformer.ts b/apps/judicial-system/backend/src/app/transformers/nationalId.transformer.ts new file mode 100644 index 000000000000..9a2d80ac6444 --- /dev/null +++ b/apps/judicial-system/backend/src/app/transformers/nationalId.transformer.ts @@ -0,0 +1,13 @@ +interface Value { + value?: string | null +} + +type NationalIdTransformer = ({ value }: Value) => string | undefined | null + +export const nationalIdTransformer: NationalIdTransformer = ({ value }) => { + if (!value) { + return value + } + + return value.replace(/-/g, '') +} diff --git a/apps/judicial-system/robot-api/.eslintrc.json b/apps/judicial-system/robot-api/.eslintrc.json deleted file mode 100644 index eb74b2b984ef..000000000000 --- a/apps/judicial-system/robot-api/.eslintrc.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "extends": "../../../.eslintrc.json", - "rules": { - "simple-import-sort/imports": [ - "error", - { - "groups": [ - // External imports come first. - ["^\\w", "@(?!nestjs|island\\.is)"], - // Then nestjs packages. - ["^@nestjs"], - // Then island.is packages. - ["^@island\\.is/(?!judicial-system)"], - // Then judicial-system packages. - ["^@island\\.is/judicial-system"], - // Then all other imports in this order: "../", "./", "./strings", "./css" - [ - "^\\.\\.(?!/?$)", - "^\\.\\./?$", - "^\\./(?=.*/)(?!/?$)", - "^\\.(?!/?$)", - "^\\./?$" - ] - ] - } - ] - }, - "plugins": ["simple-import-sort"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": {} }, - { "files": ["*.ts", "*.tsx"], "rules": {} }, - { "files": ["*.js", "*.jsx"], "rules": {} } - ] -} diff --git a/apps/judicial-system/robot-api/esbuild.json b/apps/judicial-system/robot-api/esbuild.json deleted file mode 100644 index ed8694e4a00e..000000000000 --- a/apps/judicial-system/robot-api/esbuild.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "platform": "node", - "external": [ - "fsevents", - "@nestjs/microservices", - "class-transformer", - "cache-manager", - "@nestjs/websockets/socket-module", - "class-validator", - "class-transformer", - "@nestjs/microservices/microservices-module", - "apollo-server-fastify", - "@elastic/elasticsearch", - "fastify-swagger", - "@nestjs/mongoose", - "@nestjs/typeorm", - "dd-trace", - "express", - "http-errors", - "graphql", - "pg", - "winston", - "util-deprecate", - "source-map-resolve", - "atob", - "logform", - "pg-native", - "form-data", - "swagger-ui-dist", - "@protobufjs/aspromise", - "@protobufjs/base64", - "@protobufjs/codegen", - "@protobufjs/eventemitter", - "@protobufjs/fetch", - "@protobufjs/float", - "@protobufjs/inquire", - "@protobufjs/path", - "@protobufjs/pool", - "@protobufjs/utf8", - "safer-buffer", - "@mikro-orm/core" - ], - "keepNames": true -} diff --git a/apps/judicial-system/robot-api/infra/judicial-system-xrd-robot-api.ts b/apps/judicial-system/robot-api/infra/judicial-system-xrd-robot-api.ts deleted file mode 100644 index 9eed7a919938..000000000000 --- a/apps/judicial-system/robot-api/infra/judicial-system-xrd-robot-api.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ref, service, ServiceBuilder } from '../../../../infra/src/dsl/dsl' - -export const serviceSetup = (services: { - backend: ServiceBuilder<'judicial-system-backend'> -}): ServiceBuilder<'judicial-system-robot-api'> => - service('judicial-system-robot-api') - .namespace('judicial-system') - .serviceAccount('judicial-system-robot-api') - .image('judicial-system-robot-api') - .env({ - BACKEND_URL: ref((h) => `http://${h.svc(services.backend)}`), - AUDIT_TRAIL_USE_GENERIC_LOGGER: 'false', - AUDIT_TRAIL_GROUP_NAME: 'k8s/judicial-system/audit-log', - AUDIT_TRAIL_REGION: 'eu-west-1', - }) - .secrets({ - ERROR_EVENT_URL: '/k8s/judicial-system/ERROR_EVENT_URL', - BACKEND_ACCESS_TOKEN: '/k8s/judicial-system/BACKEND_ACCESS_TOKEN', - }) - .liveness('/liveness') - .readiness('/liveness') - .ingress({ - primary: { - host: { - dev: 'judicial-system-robot-api-xrd', - staging: 'judicial-system-robot-api-xrd', - prod: 'judicial-system-robot-api-xrd', - }, - paths: ['/'], - public: false, - }, - }) - .grantNamespaces('nginx-ingress-internal') diff --git a/apps/judicial-system/robot-api/jest.config.ts b/apps/judicial-system/robot-api/jest.config.ts deleted file mode 100644 index f306ee005a12..000000000000 --- a/apps/judicial-system/robot-api/jest.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable */ -export default { - preset: './jest.preset.js', - rootDir: '../../..', - roots: [__dirname], - globals: {}, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '/coverage/apps/judicial-system/robot-api', - displayName: 'judicial-system-robot-api', - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': [ - 'ts-jest', - { - tsconfig: `${__dirname}/tsconfig.spec.json`, - isolatedModules: true, - }, - ], - }, -} diff --git a/apps/judicial-system/robot-api/project.json b/apps/judicial-system/robot-api/project.json deleted file mode 100644 index 181727e4a47e..000000000000 --- a/apps/judicial-system/robot-api/project.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "judicial-system-robot-api", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "apps/judicial-system/robot-api/src", - "projectType": "application", - "prefix": "judicial-system-robot-api", - "tags": ["scope:judicial-system-api"], - "generators": {}, - "targets": { - "build": { - "executor": "./tools/executors/node:build", - "options": { - "outputPath": "dist/apps/judicial-system/robot-api", - "main": "apps/judicial-system/robot-api/src/main.ts", - "tsConfig": "apps/judicial-system/robot-api/tsconfig.app.json", - "assets": [], - "maxWorkers": 2 - }, - "configurations": { - "production": { - "optimization": true, - "extractLicenses": true, - "inspect": false - } - } - }, - "serve": { - "executor": "@nx/js:node", - "options": { - "buildTarget": "judicial-system-robot-api:build" - } - }, - "lint": { - "executor": "@nx/eslint:lint" - }, - "test": { - "executor": "@nx/jest:jest", - "options": { - "jestConfig": "apps/judicial-system/robot-api/jest.config.ts" - } - }, - "codegen/backend-schema": { - "executor": "nx:run-commands", - "options": { - "command": "cross-env INIT_SCHEMA=true yarn ts-node -P apps/judicial-system/robot-api/tsconfig.app.json apps/judicial-system/robot-api/src/buildOpenApi.ts" - }, - "outputs": ["{projectRoot}/src/openapi.yaml"] - }, - "docker-express": { - "executor": "Intentionally left blank, only so this target is valid when using `nx show projects --with-target docker-express`" - } - } -} diff --git a/apps/judicial-system/robot-api/src/app/app.config.ts b/apps/judicial-system/robot-api/src/app/app.config.ts deleted file mode 100644 index 5c52fbad0eac..000000000000 --- a/apps/judicial-system/robot-api/src/app/app.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from '@island.is/nest/config' - -export default defineConfig({ - name: 'AppModule', - load: (env) => ({ - errorReportUrl: env.required('ERROR_EVENT_URL', ''), - backend: { - accessToken: env.required( - 'BACKEND_ACCESS_TOKEN', - 'secret-backend-api-token', - ), - url: env.required('BACKEND_URL', 'http://localhost:3344'), - }, - }), -}) diff --git a/apps/judicial-system/robot-api/src/app/app.controller.ts b/apps/judicial-system/robot-api/src/app/app.controller.ts deleted file mode 100644 index 5bda74f60457..000000000000 --- a/apps/judicial-system/robot-api/src/app/app.controller.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Controller, Get, Inject } from '@nestjs/common' -import { ApiCreatedResponse } from '@nestjs/swagger' - -import type { Logger } from '@island.is/logging' -import { LOGGER_PROVIDER } from '@island.is/logging' - -import { AppService } from './app.service' - -@Controller('api') -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(): Promise { - this.logger.debug('Testing connection') - - return this.appService.testConnection() - } -} diff --git a/apps/judicial-system/robot-api/src/app/app.module.ts b/apps/judicial-system/robot-api/src/app/app.module.ts deleted file mode 100644 index 9ebdfe08242e..000000000000 --- a/apps/judicial-system/robot-api/src/app/app.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Module } from '@nestjs/common' -import { ConfigModule } from '@nestjs/config' - -import { ProblemModule } from '@island.is/nest/problem' - -import { - AuditTrailModule, - auditTrailModuleConfig, -} from '@island.is/judicial-system/audit-trail' - -import appModuleConfig from './app.config' -import { AppController } from './app.controller' -import { AppService } from './app.service' - -@Module({ - imports: [ - AuditTrailModule, - ProblemModule.forRoot({ logAllErrors: true }), - ConfigModule.forRoot({ - isGlobal: true, - load: [appModuleConfig, auditTrailModuleConfig], - }), - ], - controllers: [AppController], - providers: [AppService], -}) -export class AppModule {} diff --git a/apps/judicial-system/robot-api/src/app/app.service.ts b/apps/judicial-system/robot-api/src/app/app.service.ts deleted file mode 100644 index 4b935505d940..000000000000 --- a/apps/judicial-system/robot-api/src/app/app.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common' - -import type { Logger } from '@island.is/logging' -import { LOGGER_PROVIDER } from '@island.is/logging' -import { type ConfigType } from '@island.is/nest/config' - -import { AuditTrailService } from '@island.is/judicial-system/audit-trail' - -import appModuleConfig from './app.config' - -@Injectable() -export class AppService { - constructor( - @Inject(appModuleConfig.KEY) - private readonly config: ConfigType, - @Inject(LOGGER_PROVIDER) private readonly logger: Logger, - private readonly auditTrailService: AuditTrailService, - ) {} - - private async test(): Promise { - return 'OK' - } - - async testConnection(): Promise { - //TODO: Audit - return this.test() - } -} diff --git a/apps/judicial-system/robot-api/src/app/index.ts b/apps/judicial-system/robot-api/src/app/index.ts deleted file mode 100644 index a49c6ea5487c..000000000000 --- a/apps/judicial-system/robot-api/src/app/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AppModule } from './app.module' diff --git a/apps/judicial-system/robot-api/src/buildOpenApi.ts b/apps/judicial-system/robot-api/src/buildOpenApi.ts deleted file mode 100644 index 9a54cdcb918b..000000000000 --- a/apps/judicial-system/robot-api/src/buildOpenApi.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { buildOpenApi } from '@island.is/infra-nest-server' - -import { AppModule } from './app/app.module' -import { openApi } from './openApi' - -buildOpenApi({ - path: 'apps/judicial-system/robot-api/src/openapi.yaml', - appModule: AppModule, - openApi, -}) diff --git a/apps/judicial-system/robot-api/src/main.ts b/apps/judicial-system/robot-api/src/main.ts deleted file mode 100644 index f41d5432d2f0..000000000000 --- a/apps/judicial-system/robot-api/src/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { bootstrap } from '@island.is/infra-nest-server' - -import { AppModule } from './app' -import { openApi } from './openApi' - -bootstrap({ - appModule: AppModule, - name: 'judicial-system-robot-api', - port: 3356, - swaggerPath: 'api/swagger', - openApi, -}) diff --git a/apps/judicial-system/robot-api/src/openApi.ts b/apps/judicial-system/robot-api/src/openApi.ts deleted file mode 100644 index 3402ffe5a476..000000000000 --- a/apps/judicial-system/robot-api/src/openApi.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { DocumentBuilder } from '@nestjs/swagger' - -export const openApi = new DocumentBuilder() - .setTitle('Judicial System xRoad robot API') - .setDescription('This is the xRoad robot API for the judicial system.') - .setVersion('1.0') - .addTag('judicial-system') - .build() diff --git a/apps/judicial-system/robot-api/tsconfig.app.json b/apps/judicial-system/robot-api/tsconfig.app.json deleted file mode 100644 index 5479e745834a..000000000000 --- a/apps/judicial-system/robot-api/tsconfig.app.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "types": ["node"] - }, - "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/test/**", "jest.config.ts"], - "include": ["**/*.ts"] -} diff --git a/apps/judicial-system/robot-api/tsconfig.json b/apps/judicial-system/robot-api/tsconfig.json deleted file mode 100644 index 25873177db3f..000000000000 --- a/apps/judicial-system/robot-api/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.app.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/apps/judicial-system/robot-api/tsconfig.spec.json b/apps/judicial-system/robot-api/tsconfig.spec.json deleted file mode 100644 index e87789c595d9..000000000000 --- a/apps/judicial-system/robot-api/tsconfig.spec.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "**/*.spec.ts", - "**/*.test.ts", - "**/test/*.ts", - "**/*.d.ts", - "jest.config.ts" - ] -} diff --git a/apps/judicial-system/web/messages/Core/errors.ts b/apps/judicial-system/web/messages/Core/errors.ts index 1333b3f2484d..b2388cfda63e 100644 --- a/apps/judicial-system/web/messages/Core/errors.ts +++ b/apps/judicial-system/web/messages/Core/errors.ts @@ -162,4 +162,10 @@ export const errors = defineMessages({ defaultMessage: 'Dagsetning ekki rétt slegin inn', description: 'Notaður sem villuskilaboð þegar dagsetning er ekki löggild', }, + uploadFailed: { + id: 'judicial.system.core:errors.upload_failed', + defaultMessage: 'Upp kom villa við að hlaða upp skjali', + description: + 'Notaður sem villuskilaboð þegar ekki tekst að hlaða upp skjali', + }, }) diff --git a/apps/judicial-system/web/pages/rikissaksoknari/akaera/senda-til-fmst/[id]/[defendantId].ts b/apps/judicial-system/web/pages/rikissaksoknari/akaera/senda-til-fmst/[id]/[defendantId].ts new file mode 100644 index 000000000000..a5a2b497f1be --- /dev/null +++ b/apps/judicial-system/web/pages/rikissaksoknari/akaera/senda-til-fmst/[id]/[defendantId].ts @@ -0,0 +1,3 @@ +import SendToPrisonAdmin from '@island.is/judicial-system-web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin' + +export default SendToPrisonAdmin diff --git a/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.spec.tsx b/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.spec.tsx new file mode 100644 index 000000000000..48ac69307bbc --- /dev/null +++ b/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.spec.tsx @@ -0,0 +1,80 @@ +import faker from 'faker' +import { render, screen } from '@testing-library/react' + +import { + CaseIndictmentRulingDecision, + CaseType, + Defendant, +} from '../../graphql/schema' +import { mockCase } from '../../utils/mocks' +import { + ApolloProviderWrapper, + FormContextWrapper, + IntlProviderWrapper, +} from '../../utils/testHelpers' +import BlueBoxWithDate from './BlueBoxWithDate' + +jest.mock('next/router', () => ({ + useRouter() { + return { + pathname: '', + query: { + id: 'test_id', + }, + } + }, +})) + +describe('BlueBoxWithDate', () => { + const name = faker.name.firstName() + const rulingDate = new Date().toISOString() + + const mockDefendant: Defendant = { + name, + id: faker.datatype.uuid(), + } + + it('renders correctly when ruling decision is FINE', () => { + render( + + + + + + + , + ) + + expect(screen.getByText('Viðurlagaákvörðun')).toBeInTheDocument() + expect(screen.getByText(name)).toBeInTheDocument() + }) + + it('renders correctly when ruling decision is RULING', () => { + render( + + + + + + + , + ) + + expect(screen.getByText('Birting dóms')).toBeInTheDocument() + expect(screen.getByText(name)).toBeInTheDocument() + }) +}) diff --git a/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.strings.ts b/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.strings.ts index eae8e0f09ca1..a3f57cbebd4c 100644 --- a/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.strings.ts +++ b/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.strings.ts @@ -2,8 +2,8 @@ import { defineMessages } from 'react-intl' export const strings = defineMessages({ keyDates: { - id: 'judicial.system.core:blue_box_with_date.key_dates', - defaultMessage: 'Lykildagsetningar', + id: 'judicial.system.core:blue_box_with_date.key_dates_v1', + defaultMessage: 'Birting dóms', description: 'Notaður sem titill í lykildagsetningarsvæði dómfellda.', }, defendantVerdictViewDateLabel: { @@ -52,4 +52,35 @@ export const strings = defineMessages({ description: 'Notaður sem text í takka til að skrá hvenær dómur var áfrýjaður af dómfellda.', }, + sendToPrisonAdmin: { + id: 'judicial.system.core:blue_box_with_date.send_to_fmst', + defaultMessage: 'Senda til fullnustu', + description: + 'Notaður sem texti í valmynd fyrir aðgerðina að senda mál til fullnustu', + }, + revokeSendToPrisonAdmin: { + id: 'judicial.system.core:blue_box_with_date.revoke_send_to_fmst', + defaultMessage: 'Afturkalla úr fullnustu', + description: + 'Notaður sem texti í valmynd fyrir aðgerðina að senda mál til fullnustu', + }, + sendToPrisonAdminDate: { + id: 'judicial.system.core:blue_box_with_date.send_to_fmst_date', + defaultMessage: 'Sent til fullnustu {date}', + description: + 'Notaður sem texti í valmynd fyrir aðgerðina að senda mál til fullnustu', + }, + indictmentRulingDecisionFine: { + id: 'judicial.system.core:blue_box_with_date.indictment_ruling_decision_fine', + defaultMessage: 'Viðurlagaákvörðun', + description: + 'Notaður sem titill í svæði þar sem kærufrestur viðurlagaákvörðunar er tekinn fram', + }, + fineAppealDeadline: { + id: 'judicial.system.core:blue_box_with_date.fine_appeal_deadline', + defaultMessage: + 'Kærufrestur Ríkissaksóknara {appealDeadlineIsInThePast, select, true {var} other {er}} til {appealDeadline}', + description: + 'Notaður sem titill í svæði þar sem kærufrestur viðurlagaákvörðunar er tekinn fram', + }, }) diff --git a/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.tsx b/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.tsx index f5e499d305b7..323fe5cf4469 100644 --- a/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.tsx +++ b/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.tsx @@ -1,7 +1,8 @@ -import { FC, useContext, useEffect, useState } from 'react' +import { FC, useContext, useMemo, useState } from 'react' import { useIntl } from 'react-intl' import addDays from 'date-fns/addDays' import { AnimatePresence, motion } from 'framer-motion' +import { useRouter } from 'next/router' import { Box, @@ -11,6 +12,7 @@ import { Text, toast, } from '@island.is/island-ui/core' +import { PUBLIC_PROSECUTOR_STAFF_INDICTMENT_SEND_TO_PRISON_ADMIN_ROUTE } from '@island.is/judicial-system/consts' import { formatDate } from '@island.is/judicial-system/formatters' import { VERDICT_APPEAL_WINDOW_DAYS } from '@island.is/judicial-system/types' import { errors } from '@island.is/judicial-system-web/messages' @@ -30,22 +32,27 @@ import * as styles from './BlueBoxWithIcon.css' interface Props { defendant: Defendant - indictmentRulingDecision?: CaseIndictmentRulingDecision | null icon?: IconMapIcon } -type DateType = 'verdictViewDate' | 'appealDate' - const BlueBoxWithDate: FC = (props) => { - const { defendant, indictmentRulingDecision, icon } = props + const { defendant, icon } = props const { formatMessage } = useIntl() - const [verdictViewDate, setVerdictViewDate] = useState() - const [appealDate, setAppealDate] = useState() - const [textItems, setTextItems] = useState([]) + const [dates, setDates] = useState<{ + verdictViewDate?: Date + verdictAppealDate?: Date + }>({ + verdictViewDate: undefined, + verdictAppealDate: undefined, + }) const [triggerAnimation, setTriggerAnimation] = useState(false) const [triggerAnimation2, setTriggerAnimation2] = useState(false) const { setAndSendDefendantToServer } = useDefendants() const { workingCase, setWorkingCase } = useContext(FormContext) + const router = useRouter() + + const isFine = + workingCase.indictmentRulingDecision === CaseIndictmentRulingDecision.FINE const serviceRequired = defendant.serviceRequirement === ServiceRequirement.REQUIRED @@ -53,7 +60,7 @@ const BlueBoxWithDate: FC = (props) => { const handleDateChange = ( date: Date | undefined, valid: boolean, - dateType: DateType, + type: keyof typeof dates, ) => { if (!date) { // Do nothing @@ -65,43 +72,125 @@ const BlueBoxWithDate: FC = (props) => { return } - dateType === 'verdictViewDate' - ? setVerdictViewDate(date) - : setAppealDate(date) + setDates((prev) => ({ ...prev, [type]: date })) } - const handleSetDate = (dateType: DateType) => { - if ( - (dateType === 'verdictViewDate' && !verdictViewDate) || - (dateType === 'appealDate' && !appealDate) - ) { + const handleSetDate = (type: keyof typeof dates) => { + const date = dates[type] + + if (!date) { toast.error(formatMessage(errors.invalidDate)) return } - if (dateType === 'verdictViewDate' && verdictViewDate) { - setAndSendDefendantToServer( - { - caseId: workingCase.id, - defendantId: defendant.id, - verdictViewDate: formatDateForServer(verdictViewDate), - }, - setWorkingCase, - ) - } else if (dateType === 'appealDate' && appealDate) { + const payload = { + caseId: workingCase.id, + defendantId: defendant.id, + [type]: formatDateForServer(date), + } + + setAndSendDefendantToServer(payload, setWorkingCase) + + if (type === 'verdictAppealDate') { setTriggerAnimation2(true) - setAndSendDefendantToServer( - { - caseId: workingCase.id, - defendantId: defendant.id, - verdictAppealDate: formatDateForServer(appealDate), - }, - setWorkingCase, + } + } + + const handleSendToPrisonAdmin = () => { + router.push( + `${PUBLIC_PROSECUTOR_STAFF_INDICTMENT_SEND_TO_PRISON_ADMIN_ROUTE}/${workingCase.id}/${defendant.id}`, + ) + } + + const handleRevokeSendToPrisonAdmin = () => { + setAndSendDefendantToServer( + { + caseId: workingCase.id, + defendantId: defendant.id, + isSentToPrisonAdmin: false, + }, + setWorkingCase, + ) + } + + const appealExpirationInfo = useMemo(() => { + const deadline = + defendant.verdictAppealDeadline || + (dates.verdictViewDate && + addDays( + dates.verdictViewDate, + VERDICT_APPEAL_WINDOW_DAYS, + ).toISOString()) + + return getAppealExpirationInfo( + deadline, + defendant.isVerdictAppealDeadlineExpired, + ) + }, [ + dates.verdictViewDate, + defendant.isVerdictAppealDeadlineExpired, + defendant.verdictAppealDeadline, + ]) + + const textItems = useMemo(() => { + const texts = [] + + if (isFine) { + texts.push( + formatMessage(strings.fineAppealDeadline, { + appealDeadlineIsInThePast: defendant.isVerdictAppealDeadlineExpired, + appealDeadline: formatDate(defendant.verdictAppealDeadline), + }), ) } else { - toast.error(formatMessage(errors.general)) + if (serviceRequired) { + texts.push( + formatMessage(strings.defendantVerdictViewedDate, { + date: formatDate( + dates.verdictViewDate ?? defendant.verdictViewDate, + ), + }), + ) + } + + texts.push( + formatMessage(appealExpirationInfo.message, { + appealExpirationDate: appealExpirationInfo.date, + }), + ) + + if (defendant.verdictAppealDate) { + texts.push( + formatMessage(strings.defendantAppealDate, { + date: formatDate(defendant.verdictAppealDate), + }), + ) + } + + if (defendant.sentToPrisonAdminDate && defendant.isSentToPrisonAdmin) { + texts.push( + formatMessage(strings.sendToPrisonAdminDate, { + date: formatDate(defendant.sentToPrisonAdminDate), + }), + ) + } } - } + + return texts + }, [ + appealExpirationInfo.date, + appealExpirationInfo.message, + dates.verdictViewDate, + defendant.isSentToPrisonAdmin, + defendant.isVerdictAppealDeadlineExpired, + defendant.sentToPrisonAdminDate, + defendant.verdictAppealDate, + defendant.verdictAppealDeadline, + defendant.verdictViewDate, + formatMessage, + isFine, + serviceRequired, + ]) const datePickerVariants = { dpHidden: { opacity: 0, y: 15, marginTop: '16px' }, @@ -127,162 +216,146 @@ const BlueBoxWithDate: FC = (props) => { }, } - useEffect(() => { - const verdictAppealDeadline = defendant.verdictAppealDeadline - ? defendant.verdictAppealDeadline - : verdictViewDate - ? addDays( - new Date(verdictViewDate), - VERDICT_APPEAL_WINDOW_DAYS, - ).toISOString() - : null - - const appealExpiration = getAppealExpirationInfo( - verdictAppealDeadline, - defendant.isVerdictAppealDeadlineExpired, - ) - - setTextItems([ - ...(serviceRequired - ? [ - formatMessage(strings.defendantVerdictViewedDate, { - date: verdictViewDate - ? formatDate(verdictViewDate) - : formatDate(defendant.verdictViewDate), - }), - ] - : []), - formatMessage(appealExpiration.message, { - appealExpirationDate: appealExpiration.date, - }), - ...(defendant.verdictAppealDate - ? [ - formatMessage(strings.defendantAppealDate, { - date: formatDate(defendant.verdictAppealDate), - }), - ] - : []), - ]) - }, [ - defendant.isVerdictAppealDeadlineExpired, - defendant.verdictAppealDate, - defendant.verdictAppealDeadline, - defendant.verdictViewDate, - formatMessage, - indictmentRulingDecision, - serviceRequired, - verdictViewDate, - ]) - return ( - - - - {icon && ( - - )} - - {defendant.name} + <> + + + + {icon && ( + + )} + + {defendant.name} + - - - {(!serviceRequired || defendant.verdictViewDate) && - textItems.map((text, index) => ( + + {(!serviceRequired || defendant.verdictViewDate || isFine) && + textItems.map((text, index) => ( + setTriggerAnimation(true)} + > + {`• ${text}`} + + ))} + + + {defendant.verdictAppealDate || + defendant.isVerdictAppealDeadlineExpired || + isFine ? null : !serviceRequired || defendant.verdictViewDate ? ( + + + + handleDateChange(date, valid, 'verdictAppealDate') + } + maxDate={new Date()} + blueBox={false} + dateOnly + /> + + + + ) : ( setTriggerAnimation(true)} + key="defendantVerdictViewDate" + variants={datePickerVariants} + initial={false} + animate="dpVisible" + exit="dpExit" + transition={{ duration: 0.2, ease: 'easeInOut', delay: 0.4 }} > - {`• ${text}`} + + + handleDateChange(date, valid, 'verdictViewDate') + } + blueBox={false} + maxDate={new Date()} + dateOnly + /> + + - ))} - - - {defendant.verdictAppealDate || - defendant.isVerdictAppealDeadlineExpired ? null : !serviceRequired || - defendant.verdictViewDate ? ( - + + + {defendant.isSentToPrisonAdmin ? ( + - - + {formatMessage(strings.revokeSendToPrisonAdmin)} + ) : ( - - - - handleDateChange(date, valid, 'verdictViewDate') - } - blueBox={false} - maxDate={new Date()} - dateOnly - /> - - - + {formatMessage(strings.sendToPrisonAdmin)} + )} - - + + ) } diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql index d80ada64e561..f2a5e28a0aa0 100644 --- a/apps/judicial-system/web/src/components/FormProvider/case.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql @@ -33,6 +33,8 @@ query Case($input: CaseQueryInput!) { verdictAppealDate isVerdictAppealDeadlineExpired subpoenaType + isSentToPrisonAdmin + sentToPrisonAdminDate subpoenas { id created diff --git a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql index 1e09fc783d50..df8b33ad5c86 100644 --- a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql @@ -45,6 +45,8 @@ query LimitedAccessCase($input: CaseQueryInput!) { verdictAppealDeadline isVerdictAppealDeadlineExpired subpoenaType + isSentToPrisonAdmin + sentToPrisonAdminDate subpoenas { id created diff --git a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts index 0ef67882b9ac..3bdd0a941950 100644 --- a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts +++ b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts @@ -48,4 +48,9 @@ export const strings = defineMessages({ defaultMessage: 'Hefur ekki verið skráður', description: 'Notaður sem texti þegar enginn verjandi er skráður.', }, + sendToPrisonAdminDate: { + id: 'judicial.system.core:info_card.send_to_prison_admin_date', + defaultMessage: 'Sent til fullnustu {date}', + description: 'Notaður sem texti fyrir hvenær mál var sent til fullnustu', + }, }) diff --git a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx index 7d860a8f9f47..45a74dd89427 100644 --- a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx +++ b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx @@ -26,6 +26,7 @@ interface DefendantInfoProps { defendant: Defendant displayAppealExpirationInfo?: boolean displayVerdictViewDate?: boolean + displaySentToPrisonAdminDate?: boolean defender?: Defender } @@ -54,14 +55,11 @@ export const getAppealExpirationInfo = ( const getVerdictViewDateText = ( formatMessage: IntlShape['formatMessage'], verdictViewDate?: string | null, - serviceNotRequired?: boolean, ): string => { if (verdictViewDate) { return formatMessage(strings.verdictDisplayedDate, { date: formatDate(verdictViewDate, 'PPP'), }) - } else if (serviceNotRequired) { - return formatMessage(strings.serviceNotRequired) } else { return formatMessage(strings.serviceRequired) } @@ -72,6 +70,7 @@ export const DefendantInfo: FC = (props) => { defendant, displayAppealExpirationInfo, displayVerdictViewDate, + displaySentToPrisonAdminDate = true, defender, } = props const { formatMessage } = useIntl() @@ -129,21 +128,24 @@ export const DefendantInfo: FC = (props) => { )} {displayAppealExpirationInfo && ( - - - {formatMessage(appealExpirationInfo.message, { - appealExpirationDate: appealExpirationInfo.date, - })} - - + + {formatMessage(appealExpirationInfo.message, { + appealExpirationDate: appealExpirationInfo.date, + })} + )} - {displayVerdictViewDate && defendant.serviceRequirement && ( + {displayVerdictViewDate && + defendant.serviceRequirement && + defendant.serviceRequirement !== ServiceRequirement.NOT_REQUIRED && ( + + {getVerdictViewDateText(formatMessage, defendant.verdictViewDate)} + + )} + {displaySentToPrisonAdminDate && defendant.sentToPrisonAdminDate && ( - {getVerdictViewDateText( - formatMessage, - defendant.verdictViewDate, - defendant.serviceRequirement === ServiceRequirement.NOT_REQUIRED, - )} + {formatMessage(strings.sendToPrisonAdminDate, { + date: formatDate(defendant.sentToPrisonAdminDate), + })} )} diff --git a/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx b/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx index a2e2a8d44204..88391ea722d7 100644 --- a/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx +++ b/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx @@ -8,6 +8,7 @@ import useInfoCardItems from './useInfoCardItems' export interface Props { displayAppealExpirationInfo?: boolean displayVerdictViewDate?: boolean + displaySentToPrisonAdminDate?: boolean } const InfoCardClosedIndictment: FC = (props) => { @@ -29,7 +30,11 @@ const InfoCardClosedIndictment: FC = (props) => { civilClaimants, } = useInfoCardItems() - const { displayAppealExpirationInfo, displayVerdictViewDate } = props + const { + displayAppealExpirationInfo, + displayVerdictViewDate, + displaySentToPrisonAdminDate, + } = props const reviewedDate = workingCase.eventLogs?.find( (log) => log.eventType === EventType.INDICTMENT_REVIEWED, @@ -45,6 +50,7 @@ const InfoCardClosedIndictment: FC = (props) => { workingCase.type, displayAppealExpirationInfo, displayVerdictViewDate, + displaySentToPrisonAdminDate, ), ], }, diff --git a/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.strings.ts b/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.strings.ts index 57689f0fa174..1cff30e2a52b 100644 --- a/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.strings.ts +++ b/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.strings.ts @@ -28,14 +28,16 @@ export const strings = defineMessages({ description: 'Notaður sem titill á "ákvörðun" hluta af yfirliti ákæru.', }, reviewTagAppealed: { - id: 'judicial.system.core:info_card_indictment.review_tag_appealed_v1', - defaultMessage: 'Áfrýja dómi', + id: 'judicial.system.core:info_card_indictment.review_tag_appealed_v3', + defaultMessage: + '{isFine, select, true {Kæra viðurlagaákvörðun} other {Áfrýja dómi}}', description: 'Notað sem texti á tagg fyrir "Áfrýjun" tillögu í yfirliti ákæru.', }, reviewTagAccepted: { - id: 'judicial.system.core:info_card_indictment.review_tag_completed_v1', - defaultMessage: 'Una dómi', + id: 'judicial.system.core:info_card_indictment.review_tag_completed_v2', + defaultMessage: + 'Una {isFine, select, true {viðurlagaákvörðun} other {dómi}}', description: 'Notað sem texti á tagg fyrir "Una" tillögu í yfirliti ákæru.', }, indictmentReviewedDateTitle: { diff --git a/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx b/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx index 4490433a825c..a2112beb8c66 100644 --- a/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx +++ b/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx @@ -15,6 +15,7 @@ import { core } from '@island.is/judicial-system-web/messages' import { requestCourtDate } from '@island.is/judicial-system-web/messages' import { Case, + CaseIndictmentRulingDecision, CaseType, IndictmentCaseReviewDecision, } from '@island.is/judicial-system-web/src/graphql/schema' @@ -36,6 +37,7 @@ const useInfoCardItems = () => { caseType?: CaseType | null, displayAppealExpirationInfo?: boolean, displayVerdictViewDate?: boolean, + displaySentToPrisonAdminDate?: boolean, ): Item => { const defendants = workingCase.defendants const isMultipleDefendants = defendants && defendants.length > 1 @@ -78,6 +80,7 @@ const useInfoCardItems = () => { }} displayAppealExpirationInfo={displayAppealExpirationInfo} displayVerdictViewDate={displayVerdictViewDate} + displaySentToPrisonAdminDate={displaySentToPrisonAdminDate} /> )) @@ -277,6 +280,11 @@ const useInfoCardItems = () => { IndictmentCaseReviewDecision.ACCEPT ? strings.reviewTagAccepted : strings.reviewTagAppealed, + { + isFine: + workingCase.indictmentRulingDecision === + CaseIndictmentRulingDecision.FINE, + }, ), ], } diff --git a/apps/judicial-system/web/src/components/Inputs/InputAdvocate.tsx b/apps/judicial-system/web/src/components/Inputs/InputAdvocate.tsx index 75206b7d7e37..106c0723969e 100644 --- a/apps/judicial-system/web/src/components/Inputs/InputAdvocate.tsx +++ b/apps/judicial-system/web/src/components/Inputs/InputAdvocate.tsx @@ -51,6 +51,20 @@ interface PropertyValidation { } } +interface LawyerUpdate { + defenderName: string | null + defenderNationalId: string | null + defenderEmail: string | null + defenderPhoneNumber: string | null +} + +interface SpokespersonUpdate { + spokespersonName: string | null + spokespersonNationalId: string | null + spokespersonEmail: string | null + spokespersonPhoneNumber: string | null +} + type InputType = | 'defenderEmail' | 'defenderPhoneNumber' @@ -123,18 +137,18 @@ const InputAdvocate: FC = ({ isCivilClaim: boolean, clientId?: string | null, ) => { - let updatedLawyer = { - defenderName: '', - defenderNationalId: '', - defenderEmail: '', - defenderPhoneNumber: '', + let updatedLawyer: LawyerUpdate = { + defenderName: null, + defenderNationalId: null, + defenderEmail: null, + defenderPhoneNumber: null, } - let updatedSpokesperson = { - spokespersonName: '', - spokespersonNationalId: '', - spokespersonEmail: '', - spokespersonPhoneNumber: '', + let updatedSpokesperson: SpokespersonUpdate = { + spokespersonName: null, + spokespersonNationalId: null, + spokespersonEmail: null, + spokespersonPhoneNumber: null, } if (selectedOption) { @@ -147,16 +161,16 @@ const InputAdvocate: FC = ({ ) updatedLawyer = { defenderName: lawyer ? lawyer.name : label, - defenderNationalId: lawyer ? lawyer.nationalId : '', - defenderEmail: lawyer ? lawyer.email : '', - defenderPhoneNumber: lawyer ? lawyer.phoneNr : '', + defenderNationalId: lawyer ? lawyer.nationalId : null, + defenderEmail: lawyer ? lawyer.email : null, + defenderPhoneNumber: lawyer ? lawyer.phoneNr : null, } updatedSpokesperson = { spokespersonName: lawyer ? lawyer.name : label, - spokespersonNationalId: lawyer ? lawyer.nationalId : '', - spokespersonEmail: lawyer ? lawyer.email : '', - spokespersonPhoneNumber: lawyer ? lawyer.phoneNr : '', + spokespersonNationalId: lawyer ? lawyer.nationalId : null, + spokespersonEmail: lawyer ? lawyer.email : null, + spokespersonPhoneNumber: lawyer ? lawyer.phoneNr : null, } } @@ -226,17 +240,17 @@ const InputAdvocate: FC = ({ switch (property) { case 'defenderEmail': { return { - defenderEmail: value, + defenderEmail: value || null, } } case 'defenderPhoneNumber': { - return { defenderPhoneNumber: value } + return { defenderPhoneNumber: value || null } } case 'spokespersonEmail': { - return { spokespersonEmail: value } + return { spokespersonEmail: value || null } } case 'spokespersonPhoneNumber': { - return { spokespersonPhoneNumber: value } + return { spokespersonPhoneNumber: value || null } } } }, []) diff --git a/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx b/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx index b737255e67c0..0489d37fb744 100644 --- a/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx +++ b/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx @@ -50,16 +50,15 @@ const InputNationalId: FC = (props) => { const inputValidator = validate([ [ evt.target.value, - isDateOfBirth ? ['date-of-birth'] : ['empty', 'national-id'], ], ]) - if (!required) { - onBlur(inputValue) - } else if (inputValidator.isValid) { + if (inputValidator.isValid) { setErrorMessage(undefined) onBlur(inputValue) + } else if (!required && !evt.target.value) { + onBlur(inputValue) } else { setErrorMessage(inputValidator.errorMessage) } diff --git a/apps/judicial-system/web/src/components/Modal/Modal.tsx b/apps/judicial-system/web/src/components/Modal/Modal.tsx index f5778bbf4c5a..cce5de20aa11 100644 --- a/apps/judicial-system/web/src/components/Modal/Modal.tsx +++ b/apps/judicial-system/web/src/components/Modal/Modal.tsx @@ -22,6 +22,7 @@ interface ModalProps { errorMessage?: string children?: ReactNode invertButtonColors?: boolean + loading?: boolean } const Modal: FC> = ({ @@ -39,6 +40,7 @@ const Modal: FC> = ({ errorMessage, children, invertButtonColors, + loading, }: ModalProps) => { const modalVariants = { open: { @@ -73,7 +75,11 @@ const Modal: FC> = ({ > {onClose && ( - @@ -100,6 +106,7 @@ const Modal: FC> = ({ variant={invertButtonColors ? undefined : 'ghost'} onClick={onSecondaryButtonClick} loading={isSecondaryButtonLoading} + disabled={loading} > {secondaryButtonText} @@ -146,6 +153,7 @@ const ModalPortal = ({ errorMessage, children, invertButtonColors, + loading, }: ModalProps) => { const modalRoot = document.getElementById('modal') ?? document.createElement('div') @@ -166,6 +174,7 @@ const ModalPortal = ({ errorMessage={errorMessage} children={children} invertButtonColors={invertButtonColors} + loading={loading} />, modalRoot, ) diff --git a/apps/judicial-system/web/src/components/SectionHeading/SectionHeading.tsx b/apps/judicial-system/web/src/components/SectionHeading/SectionHeading.tsx index a223be1ba987..b2c97fed3ee6 100644 --- a/apps/judicial-system/web/src/components/SectionHeading/SectionHeading.tsx +++ b/apps/judicial-system/web/src/components/SectionHeading/SectionHeading.tsx @@ -8,7 +8,7 @@ interface Props { title: string required?: boolean tooltip?: ReactNode - description?: ReactNode + description?: ReactNode | string marginBottom?: ResponsiveProp heading?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' } @@ -31,7 +31,11 @@ const SectionHeading: FC = ({ {description && ( - {description} + {typeof description === 'string' ? ( + {description} + ) : ( + description + )} )} diff --git a/apps/judicial-system/web/src/components/Table/PastCasesTable/MobilePastCase.tsx b/apps/judicial-system/web/src/components/Table/PastCasesTable/MobilePastCase.tsx deleted file mode 100644 index c7170fd5f139..000000000000 --- a/apps/judicial-system/web/src/components/Table/PastCasesTable/MobilePastCase.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { FC, ReactNode } from 'react' -import { useIntl } from 'react-intl' -import format from 'date-fns/format' -import parseISO from 'date-fns/parseISO' - -import { Box, Text } from '@island.is/island-ui/core' -import { - displayFirstPlusRemaining, - formatDOB, -} from '@island.is/judicial-system/formatters' -import { tables } from '@island.is/judicial-system-web/messages' -import { TagCaseState } from '@island.is/judicial-system-web/src/components' -import { CaseListEntry } from '@island.is/judicial-system-web/src/graphql/schema' -import { CategoryCard } from '@island.is/judicial-system-web/src/routes/Shared/Cases/MobileCase' -import { displayCaseType } from '@island.is/judicial-system-web/src/routes/Shared/Cases/utils' - -interface Props { - theCase: CaseListEntry - onClick: () => void - isCourtRole: boolean - children: ReactNode - isLoading?: boolean -} - -const MobilePastCase: FC = ({ - theCase, - onClick, - isCourtRole, - children, - isLoading = false, -}) => { - const { formatMessage } = useIntl() - - return ( - , - ]} - isLoading={isLoading} - > - - {displayFirstPlusRemaining(theCase.policeCaseNumbers)} - - {theCase.courtCaseNumber && {theCase.courtCaseNumber}} -
- {theCase.defendants && theCase.defendants.length > 0 && ( - <> - {theCase.defendants[0].name ?? ''} - {theCase.defendants.length === 1 ? ( - - {formatDOB( - theCase.defendants[0].nationalId, - theCase.defendants[0].noNationalId, - )} - - ) : ( - {`+ ${theCase.defendants.length - 1}`} - )} - - )} - {theCase.created && ( - <> -
- - {`${formatMessage(tables.created)} ${format( - parseISO(theCase.created), - 'd.M.y', - )}`} - - - )} - {children} -
- ) -} - -export default MobilePastCase 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 b171d9d90953..507ea614fc55 100644 --- a/apps/judicial-system/web/src/components/Table/PastCasesTable/PastCasesTable.tsx +++ b/apps/judicial-system/web/src/components/Table/PastCasesTable/PastCasesTable.tsx @@ -1,10 +1,7 @@ import { FC, useContext, useMemo } from 'react' import { useIntl } from 'react-intl' -import cn from 'classnames' -import { AnimatePresence } from 'framer-motion' import { Box, Text } from '@island.is/island-ui/core' -import { theme } from '@island.is/island-ui/theme' import { capitalize } from '@island.is/judicial-system/formatters' import { isDistrictCourtUser, @@ -13,7 +10,6 @@ import { import { core, tables } from '@island.is/judicial-system-web/messages' import { CaseTag, - ContextMenu, getIndictmentCaseStateTag, TagAppealState, TagCaseState, @@ -24,62 +20,24 @@ import { CourtCaseNumber, CreatedDate, DefendantInfo, - DurationDate, getDurationDate, - SortButton, - TableContainer, - TableHeaderText, } from '@island.is/judicial-system-web/src/components/Table' -import { - Case, - CaseListEntry, -} from '@island.is/judicial-system-web/src/graphql/schema' -import { - useCaseList, - useSort, - useViewport, -} from '@island.is/judicial-system-web/src/utils/hooks' +import { CaseListEntry } from '@island.is/judicial-system-web/src/graphql/schema' +import { useContextMenu } from '../../ContextMenu/ContextMenu' import WithdrawAppealContextMenuModal, { useWithdrawAppealMenuOption, } from '../../ContextMenu/ContextMenuOptions/WithdrawAppealMenuOption' -import IconButton from '../../IconButton/IconButton' -import MobilePastCase from './MobilePastCase' -import { contextMenu } from '../../ContextMenu/ContextMenu.strings' -import * as styles from '../Table.css' +import Table from '../Table' interface Props { cases: CaseListEntry[] - loading?: boolean - testid?: string } -const PastCasesTable: FC = ({ cases, loading = false, testid }) => { +const PastCasesTable: FC = ({ cases }) => { const { formatMessage } = useIntl() const { user } = useContext(UserContext) - const { isOpeningCaseId, handleOpenCase, LoadingIndicator, showLoading } = - useCaseList() - - const getColumnValue = ( - entry: CaseListEntry, - column: keyof CaseListEntry, - ) => { - if ( - column === 'defendants' && - entry.defendants && - entry.defendants.length > 0 - ) { - return entry.defendants[0].name ?? '' - } - return entry.created - } - - const { sortedData, requestSort, getClassNamesFor, isActiveColumn } = useSort( - 'created', - 'descending', - cases, - getColumnValue, - ) + const { openCaseInNewTabMenuItem } = useContextMenu() const { withdrawAppealMenuOption, @@ -96,176 +54,112 @@ const PastCasesTable: FC = ({ cases, loading = false, testid }) => { [cases], ) - const { width } = useViewport() - - return width < theme.breakpoints.md ? ( - <> - {pastCasesData.map((theCase) => ( - - handleOpenCase(theCase.id)} - isCourtRole={false} - isLoading={isOpeningCaseId === theCase.id && showLoading} - > - - - - ))} - - ) : ( + return ( <> - - - - requestSort('defendants')} - sortAsc={getClassNamesFor('defendants') === 'ascending'} - sortDes={getClassNamesFor('defendants') === 'descending'} - isActive={isActiveColumn('defendants')} + [ + openCaseInNewTabMenuItem(row.id), + ...(shouldDisplayWithdrawAppealOption(row) + ? [withdrawAppealMenuOption(row.id)] + : []), + ]} + columns={[ + { + cell: (row) => ( + - - - - - - - - } - > - {sortedData.map((column) => { - const indictmentCaseTag = isIndictmentCase(column.type) - ? getIndictmentCaseStateTag(column as Case, user) - : null + ), + }, + { + cell: (row) => , + }, + { + cell: (row) => { + const indictmentCaseStateTag = getIndictmentCaseStateTag( + row, + user, + ) - return ( - handleOpenCase(column.id)} - key={column.id} - > - - - - - - - - - ) - })} - + + ), + }, + ]} + /> {caseToWithdraw && ( ({ })) describe('Table', () => { - it('should sort by deadline', async () => { - const user = userEvent.setup() + let user: UserEvent + beforeEach(() => { + user = userEvent.setup() + window.localStorage.clear() + }) + + const clickButtonByTestId = async (testId: string) => { + await act(async () => { + await user.click(await screen.findByTestId(testId)) + }) + } + + it('should sort by date', async () => { const thead = [ { title: 'Title', @@ -77,15 +89,64 @@ describe('Table', () => { expect(tableRows[0]).toHaveTextContent('2021-01-01T00:00:00Z') expect(tableRows[1]).toHaveTextContent('2021-01-02T00:00:00Z') - await act(async () => { - await user.click( - await screen.findByTestId('indictmentAppealDeadlineSortButton'), - ) - }) + await clickButtonByTestId('indictmentAppealDeadlineSortButton') // The second click sorts by descending order, so the first row should be the one with the latest date const tableRows2 = await screen.findAllByTestId('tableRow') expect(tableRows2[0]).toHaveTextContent('2021-01-02T00:00:00Z') expect(tableRows2[1]).toHaveTextContent('2021-01-01T00:00:00Z') }) + + it('should sort by name', async () => { + const thead = [ + { + title: 'Name', + sortable: { + isSortable: true, + key: 'defendants' as sortableTableColumn, + }, + }, + ] + + const data: CaseListEntry[] = [ + { + created: '2021-01-01T00:00:00Z', + id: faker.datatype.uuid(), + defendants: [{ id: '', nationalId: 'string', name: 'Jon Harring Sr.' }], + }, + { + created: '2021-01-02T00:00:00Z', + id: faker.datatype.uuid(), + defendants: [{ id: '', nationalId: 'string', name: 'Bono Stingsson' }], + }, + ] + + const columns: { cell: (row: CaseListEntry) => JSX.Element }[] = + data.flatMap( + (dataItem) => + dataItem.defendants?.map((defendant) => ({ + cell: () =>

{defendant.name}

, + })) || [], + ) + + render( + + +
- requestSort('created')} - sortAsc={getClassNamesFor('created') === 'ascending'} - sortDes={getClassNamesFor('created') === 'descending'} - isActive={isActiveColumn('created')} + ), + }, + { + cell: (row) => , + }, + { + cell: (row) => ( + -
- - - - - - - - - - {indictmentCaseTag ? ( - - ) : ( - - )} - - {column.appealState && ( - - )} - - - {getDurationDate( - column.state, - column.validToDate, - column.initialRulingDate, - column.rulingDate, - )} - - - {showLoading ? ( - - {isOpeningCaseId === column.id && showLoading && ( - + return ( + <> + + {isIndictmentCase(row.type) ? ( + + ) : ( + )} - - ) : ( - - handleOpenCase(column.id, true), - icon: 'open', - }, - ...(shouldDisplayWithdrawAppealOption(column) - ? [withdrawAppealMenuOption(column.id)] - : []), - ]} - menuLabel="Opna valmöguleika á máli" - disclosure={ - { - evt.stopPropagation() - }} - disabled={false} - /> - } - /> + {row.appealState && ( + + )} + + ) + }, + }, + { + cell: (row) => ( + + {getDurationDate( + row.state, + row.validToDate, + row.initialRulingDate, + row.rulingDate, )} -
+ + , + ) + + await clickButtonByTestId('defendantsSortButton') + + const tableRows = await screen.findAllByTestId('tableRow') + expect(tableRows[0]).toHaveTextContent('Bono Stingsson') + expect(tableRows[1]).toHaveTextContent('Jon Harring Sr.') + + await clickButtonByTestId('defendantsSortButton') + + const tableRows2 = await screen.findAllByTestId('tableRow') + expect(tableRows2[0]).toHaveTextContent('Jon Harring Sr.') + expect(tableRows2[1]).toHaveTextContent('Bono Stingsson') + }) }) diff --git a/apps/judicial-system/web/src/components/Table/Table.tsx b/apps/judicial-system/web/src/components/Table/Table.tsx index cb9450da3f3e..45b6398dfe04 100644 --- a/apps/judicial-system/web/src/components/Table/Table.tsx +++ b/apps/judicial-system/web/src/components/Table/Table.tsx @@ -7,7 +7,12 @@ import { AnimatePresence, motion } from 'framer-motion' import { Box, Text } from '@island.is/island-ui/core' import { theme } from '@island.is/island-ui/theme' import { formatDate } from '@island.is/judicial-system/formatters' -import { isDistrictCourtUser } from '@island.is/judicial-system/types' +import { + CaseType, + isCompletedCase, + isDistrictCourtUser, + isRestrictionCase, +} from '@island.is/judicial-system/types' import { core } from '@island.is/judicial-system-web/messages' import { CaseListEntry, CaseState } from '../../graphql/schema' @@ -18,6 +23,7 @@ import { compareLocaleIS } from '../../utils/sortHelper' import ContextMenu, { ContextMenuItem } from '../ContextMenu/ContextMenu' import IconButton from '../IconButton/IconButton' import { UserContext } from '../UserProvider/UserProvider' +import DurationDate, { getDurationDate } from './DurationDate/DurationDate' import SortButton from './SortButton/SortButton' import TableSkeleton from './TableSkeleton/TableSkeleton' import { table as strings } from './Table.strings' @@ -92,6 +98,76 @@ const Table: FC = (props) => { const { user } = useContext(UserContext) const { formatMessage } = useIntl() + const handleCaseClick = (theCase: CaseListEntry) => { + if (!onClick?.(theCase)) { + handleOpenCase(theCase.id) + } + } + + const renderProsecutorText = ( + state?: CaseState | null, + prosecutorName?: string | null, + ) => { + if ( + state && + state === CaseState.WAITING_FOR_CONFIRMATION && + prosecutorName + ) { + return ( + + {`${formatMessage(core.prosecutorPerson)}: ${prosecutorName}`} + + ) + } + return null + } + + const renderPostponedOrCourtDateText = ( + postponedIndefinitelyExplanation?: string | null, + caseState?: CaseState | null, + courtDate?: string | null, + ) => { + if (postponedIndefinitelyExplanation) { + return {formatMessage(strings.postponed)} + } + + if (!isCompletedCase(caseState) && courtDate) { + return ( + + {`${formatMessage(strings.hearing)} ${formatDate( + parseISO(courtDate), + 'd.M.y', + )} kl. ${formatDate(parseISO(courtDate), 'kk:mm')}`} + + ) + } + + return null + } + + const renderDurationDate = ( + caseType?: CaseType | null, + caseState?: CaseState | null, + validToDate?: string | null, + initialRulingDate?: string | null, + rulingDate?: string | null, + ) => { + if (isRestrictionCase(caseType) && isCompletedCase(caseState)) { + return ( + + ) + } + + return null + } + useMemo(() => { if (sortConfig) { data.sort((a: CaseListEntry, b: CaseListEntry) => { @@ -125,34 +201,23 @@ const Table: FC = (props) => { {data.map((theCase: CaseListEntry) => ( { - if (!onClick?.(theCase)) { - handleOpenCase(theCase.id) - } - }} + onClick={() => handleCaseClick(theCase)} theCase={theCase} isCourtRole={isDistrictCourtUser(user)} isLoading={isOpeningCaseId === theCase.id && showLoading} > - {theCase.state && - theCase.state === CaseState.WAITING_FOR_CONFIRMATION && ( - - {`${formatMessage(core.prosecutorPerson)}: ${ - theCase.prosecutor?.name - }`} - - )} - {theCase.postponedIndefinitelyExplanation ? ( - {formatMessage(strings.postponed)} - ) : ( - theCase.courtDate && ( - - {`${formatMessage(strings.hearing)} ${formatDate( - parseISO(theCase.courtDate), - 'd.M.y', - )} kl. ${formatDate(parseISO(theCase.courtDate), 'kk:mm')}`} - - ) + {renderProsecutorText(theCase.state, theCase.prosecutor?.name)} + {renderPostponedOrCourtDateText( + theCase.postponedIndefinitelyExplanation, + theCase.state, + theCase.courtDate, + )} + {renderDurationDate( + theCase.type, + theCase.state, + theCase.validToDate, + theCase.initialRulingDate, + theCase.rulingDate, )} @@ -192,9 +257,7 @@ const Table: FC = (props) => { aria-disabled={isOpeningCaseId === row.id || isTransitioningCase} className={styles.tableRowContainer} onClick={() => { - if (!onClick?.(row)) { - handleOpenCase(row.id) - } + handleCaseClick(row) }} data-testid="tableRow" > diff --git a/apps/judicial-system/web/src/components/Tags/utils.ts b/apps/judicial-system/web/src/components/Tags/utils.ts index 43822e53b83f..e4f1b3ac9f83 100644 --- a/apps/judicial-system/web/src/components/Tags/utils.ts +++ b/apps/judicial-system/web/src/components/Tags/utils.ts @@ -5,8 +5,8 @@ import { } from '@island.is/judicial-system/types' import { - Case, CaseIndictmentRulingDecision, + CaseListEntry, CaseState, IndictmentDecision, User, @@ -14,7 +14,7 @@ import { import { strings } from './CaseTag.strings' export const getIndictmentCaseStateTag = ( - workingCase: Case, + caseListEntry: CaseListEntry, user?: User, ): { color: TagVariant @@ -26,7 +26,7 @@ export const getIndictmentCaseStateTag = ( indictmentRulingDecision, indictmentDecision, courtDate, - } = workingCase + } = caseListEntry switch (state) { case CaseState.NEW: @@ -39,7 +39,7 @@ export const getIndictmentCaseStateTag = ( text: isDistrictCourtUser(user) ? strings.new : strings.sent, } case CaseState.RECEIVED: - return getReceivedIndictmentStateTag(indictmentDecision, courtDate?.date) + return getReceivedIndictmentStateTag(indictmentDecision, courtDate) case CaseState.COMPLETED: return getCompletedIndictmentStateTag( indictmentReviewer, diff --git a/apps/judicial-system/web/src/components/index.ts b/apps/judicial-system/web/src/components/index.ts index adf54284e0c6..5dc0a2ff8240 100644 --- a/apps/judicial-system/web/src/components/index.ts +++ b/apps/judicial-system/web/src/components/index.ts @@ -1,6 +1,7 @@ export { CourtCaseInfo, ProsecutorCaseInfo } from './CaseInfo/CaseInfo' export { default as AccordionListItem } from './AccordionListItem/AccordionListItem' export { default as BlueBox } from './BlueBox/BlueBox' +export { default as BlueBoxWithDate } from './BlueBoxWithIcon/BlueBoxWithDate' export { default as CaseDates } from './CaseDates/CaseDates' export { default as CaseFile } from './CaseFile/CaseFile' export { default as CaseFileList } from './CaseFileList/CaseFileList' diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Conclusion/Conclusion.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Conclusion/Conclusion.tsx index 1d38486bc52c..9a81ba78f477 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Conclusion/Conclusion.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Conclusion/Conclusion.tsx @@ -186,13 +186,13 @@ const Conclusion: FC = () => { if (workingCase.postponedIndefinitelyExplanation) { setPostponementReason(workingCase.postponedIndefinitelyExplanation) } - setSelectedAction(IndictmentDecision.SCHEDULING) + break case IndictmentDecision.SCHEDULING: if (workingCase.courtSessionType) { setSelectedCourtSessionType(workingCase.courtSessionType) } - setSelectedAction(IndictmentDecision.SCHEDULING) + break case IndictmentDecision.COMPLETING: if (workingCase.indictmentRulingDecision) { @@ -201,7 +201,7 @@ const Conclusion: FC = () => { setSelectedAction(IndictmentDecision.COMPLETING) break default: - setSelectedAction(IndictmentDecision.SCHEDULING) + return } }, [ workingCase.courtSessionType, @@ -210,6 +210,15 @@ const Conclusion: FC = () => { workingCase.postponedIndefinitelyExplanation, ]) + useEffect(() => { + if ( + workingCase.indictmentDecision && + workingCase.indictmentDecision !== IndictmentDecision.COMPLETING + ) { + setSelectedAction(IndictmentDecision.SCHEDULING) + } + }, [workingCase.indictmentDecision]) + const stepIsValid = () => { // Do not leave any downloads unfinished if (!allFilesDoneOrError) { diff --git a/apps/judicial-system/web/src/routes/Court/RestrictionCase/HearingArrangements/HearingArrangements.tsx b/apps/judicial-system/web/src/routes/Court/RestrictionCase/HearingArrangements/HearingArrangements.tsx index 1746dab1ae24..29c8ac9ab0d6 100644 --- a/apps/judicial-system/web/src/routes/Court/RestrictionCase/HearingArrangements/HearingArrangements.tsx +++ b/apps/judicial-system/web/src/routes/Court/RestrictionCase/HearingArrangements/HearingArrangements.tsx @@ -33,6 +33,11 @@ import { isCourtHearingArrangemenstStepValidRC } from '@island.is/judicial-syste import { rcHearingArrangements as m } from './HearingArrangements.strings' +enum ModalButtonLoading { + PRIMARY = 'PRIMARY', + SECONDARY = 'SECONDARY', +} + export const HearingArrangements = () => { const { workingCase, @@ -43,6 +48,8 @@ export const HearingArrangements = () => { } = useContext(FormContext) const [navigateTo, setNavigateTo] = useState() + const [modalButtonLoading, setModalButtonLoading] = + useState() const { setAndSendCaseToServer, @@ -209,13 +216,23 @@ export const HearingArrangements = () => { courtDateHasChanged, }, )} - isPrimaryButtonLoading={isSendingNotification} + isPrimaryButtonLoading={ + isSendingNotification && + modalButtonLoading === ModalButtonLoading.PRIMARY + } + isSecondaryButtonLoading={ + isSendingNotification && + modalButtonLoading === ModalButtonLoading.SECONDARY + } onSecondaryButtonClick={() => { - sendNotification(workingCase.id, NotificationType.COURT_DATE, true) + setModalButtonLoading(ModalButtonLoading.SECONDARY) + sendNotification(workingCase.id, NotificationType.COURT_DATE, true) router.push(`${navigateTo}/${workingCase.id}`) }} onPrimaryButtonClick={async () => { + setModalButtonLoading(ModalButtonLoading.PRIMARY) + const notificationSent = await sendNotification( workingCase.id, NotificationType.COURT_DATE, diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Defendant/Defendant.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Defendant/Defendant.tsx index fc2b4b10cbd2..2bae92a4bbd9 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Defendant/Defendant.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Defendant/Defendant.tsx @@ -351,7 +351,7 @@ const Defendant = () => { gender: defendant.gender, name: defendant.name, address: defendant.address, - nationalId: defendant.nationalId, + nationalId: defendant.nationalId || null, noNationalId: defendant.noNationalId, citizenship: defendant.citizenship, }) @@ -361,7 +361,7 @@ const Defendant = () => { gender: defendant.gender, name: defendant.name, address: defendant.address, - nationalId: defendant.nationalId, + nationalId: defendant.nationalId || null, noNationalId: defendant.noNationalId, citizenship: defendant.citizenship, }) @@ -421,7 +421,7 @@ const Defendant = () => { gender: undefined, name: '', address: '', - nationalId: '', + nationalId: null, citizenship: '', }) @@ -442,7 +442,7 @@ const Defendant = () => { id: defendantId || uuid(), gender: undefined, name: '', - nationalId: '', + nationalId: null, address: '', citizenship: '', } as TDefendant, 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 c2a21ccaf8a9..b46712f2e366 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 @@ -75,7 +75,7 @@ const Processing: FC = () => { const router = useRouter() const isTrafficViolationCaseCheck = isTrafficViolationCase(workingCase) const [civilClaimantNationalIdUpdate, setCivilClaimantNationalIdUpdate] = - useState<{ nationalId: string; civilClaimantId: string }>() + useState<{ nationalId: string | null; civilClaimantId: string }>() const [hasCivilClaimantChoice, setHasCivilClaimantChoice] = useState() const [nationalIdNotFound, setNationalIdNotFound] = useState(false) @@ -187,12 +187,12 @@ const Processing: FC = () => { handleUpdateCivilClaimant({ caseId: workingCase.id, civilClaimantId, - nationalId, + nationalId: nationalId || null, }) } else { const cleanNationalId = nationalId ? nationalId.replace('-', '') : '' setCivilClaimantNationalIdUpdate({ - nationalId: cleanNationalId, + nationalId: cleanNationalId || null, civilClaimantId, }) } @@ -258,17 +258,20 @@ const Processing: FC = () => { ) useEffect(() => { - if (!personData || !personData.items || personData.items.length === 0) { - setNationalIdNotFound(true) + if (!civilClaimantNationalIdUpdate) { return } - setNationalIdNotFound(false) + const items = personData?.items || [] + const person = items[0] + + setNationalIdNotFound(items.length === 0) + const update = { caseId: workingCase.id, - civilClaimantId: civilClaimantNationalIdUpdate?.civilClaimantId || '', - name: personData?.items[0].name, - nationalId: personData.items[0].kennitala, + civilClaimantId: civilClaimantNationalIdUpdate.civilClaimantId || '', + nationalId: civilClaimantNationalIdUpdate.nationalId, + ...(person?.name ? { name: person.name } : {}), } handleUpdateCivilClaimant(update) diff --git a/apps/judicial-system/web/src/routes/Prosecutor/InvestigationCase/Defendant/Defendant.tsx b/apps/judicial-system/web/src/routes/Prosecutor/InvestigationCase/Defendant/Defendant.tsx index 58d28cb6146b..2be75ba20374 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/InvestigationCase/Defendant/Defendant.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/InvestigationCase/Defendant/Defendant.tsx @@ -94,7 +94,7 @@ const Defendant = () => { gender: defendant.gender, name: defendant.name, address: defendant.address, - nationalId: defendant.nationalId, + nationalId: defendant.nationalId || null, noNationalId: defendant.noNationalId, citizenship: defendant.citizenship, }) @@ -104,7 +104,7 @@ const Defendant = () => { gender: defendant.gender, name: defendant.name, address: defendant.address, - nationalId: defendant.nationalId, + nationalId: defendant.nationalId || null, noNationalId: defendant.noNationalId, citizenship: defendant.citizenship, }) @@ -336,6 +336,7 @@ const Defendant = () => { setWorkingCase, ) } + maxLength={255} /> diff --git a/apps/judicial-system/web/src/routes/Prosecutor/RestrictionCase/Defendant/Defendant.tsx b/apps/judicial-system/web/src/routes/Prosecutor/RestrictionCase/Defendant/Defendant.tsx index b7e0f94d90e4..44fe9de9b4d6 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/RestrictionCase/Defendant/Defendant.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/RestrictionCase/Defendant/Defendant.tsx @@ -107,7 +107,7 @@ export const Defendant = () => { gender: workingCase.defendants[0].gender, name: workingCase.defendants[0].name, address: workingCase.defendants[0].address, - nationalId: workingCase.defendants[0].nationalId, + nationalId: workingCase.defendants[0].nationalId || null, noNationalId: workingCase.defendants[0].noNationalId, citizenship: workingCase.defendants[0].citizenship, }) diff --git a/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx b/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx index ab612e15b6c0..3e4149d6dfb1 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx @@ -190,7 +190,7 @@ const DefendantInfo: FC = (props) => { onChange({ caseId: workingCase.id, defendantId: defendant.id, - nationalId: value, + nationalId: value || null, }) } onChange={(value) => @@ -198,7 +198,7 @@ const DefendantInfo: FC = (props) => { { caseId: workingCase.id, defendantId: defendant.id, - nationalId: value, + nationalId: value || null, }, setWorkingCase, ) diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.strings.ts b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.strings.ts index bda2b5b7b75f..3418534bf794 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.strings.ts +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.strings.ts @@ -22,9 +22,9 @@ export const strings = defineMessages({ description: 'Notaður sem titill á yfirliti ákæru.', }, reviewerSubtitle: { - id: 'judicial.system.core:public_prosecutor.indictments.overview.reviewer_subtitle', + id: 'judicial.system.core:public_prosecutor.indictments.overview.reviewer_subtitle_v2', defaultMessage: - 'Frestur til að áfrýja dómi rennur út {indictmentAppealDeadline}', + 'Frestur til að {isFine, select, true {kæra viðurlagaákvörðun} other {áfrýja dómi}} {appealDeadlineIsInThePast, select, true {rann} other {rennur}} út {indictmentAppealDeadline}', description: 'Notaður sem undirtitill á yfirliti ákæru.', }, reviewerAssignedModalTitle: { diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx index 888fcdaf4d1c..36abf477db77 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx @@ -8,6 +8,7 @@ import { formatDate } from '@island.is/judicial-system/formatters' import { core, titles } from '@island.is/judicial-system-web/messages' import { BlueBox, + BlueBoxWithDate, CourtCaseInfo, FormContentContainer, FormContext, @@ -23,8 +24,8 @@ import { // useIndictmentsLawsBroken, NOTE: Temporarily hidden while list of laws broken is not complete UserContext, } from '@island.is/judicial-system-web/src/components' -import BlueBoxWithDate from '@island.is/judicial-system-web/src/components/BlueBoxWithIcon/BlueBoxWithDate' import { useProsecutorSelectionUsersQuery } from '@island.is/judicial-system-web/src/components/ProsecutorSelection/prosecutorSelectionUsers.generated' +import { CaseIndictmentRulingDecision } from '@island.is/judicial-system-web/src/graphql/schema' import { useCase } from '@island.is/judicial-system-web/src/utils/hooks' import { strings } from './Overview.strings' @@ -102,15 +103,11 @@ export const Overview = () => { {workingCase.defendants?.map((defendant) => ( - + ))} - + {/* NOTE: Temporarily hidden while list of laws broken is not complete in @@ -130,9 +127,14 @@ export const Overview = () => { description={ {fm(strings.reviewerSubtitle, { + isFine: + workingCase.indictmentRulingDecision === + CaseIndictmentRulingDecision.FINE, indictmentAppealDeadline: formatDate( workingCase.indictmentAppealDeadline, ), + appealDeadlineIsInThePast: + workingCase.indictmentVerdictAppealDeadlineExpired, })} } diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.strings.ts b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.strings.ts new file mode 100644 index 000000000000..6a513f738833 --- /dev/null +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.strings.ts @@ -0,0 +1,44 @@ +import { defineMessages } from 'react-intl' + +export const strings = defineMessages({ + title: { + id: 'judicial.system.core:send_to_prison_admin.title', + defaultMessage: 'Senda til fullnustu', + description: 'Notaður sem titill á senda til fullnustu.', + }, + fileUploadTitle: { + id: 'judicial.system.core:send_to_prison_admin.file_upload_title', + defaultMessage: 'Gögn sem þurfa að fylgja með til fullnustu', + description: 'Notaður sem titill fyrir gagna upphlaðningu.', + }, + fileUploadDescription: { + id: 'judicial.system.core:send_to_prison_admin.file_upload_description', + defaultMessage: + 'Ef dómur er sendur til fullnustu áður en áfrýjunarfrestur ákærða er liðinn þarf að fylgja með bréf frá verjanda.', + description: 'Notaður sem texti fyrir gagna upphlaðningu.', + }, + nextButtonText: { + id: 'judicial.system.core:send_to_prison_admin.next_button_text', + defaultMessage: 'Senda til fullnustu', + description: 'Notaður sem texti í áfram takka.', + }, + modalTitle: { + id: 'judicial.system.core:send_to_prison_admin.modal_title', + defaultMessage: 'Senda til fullnustu', + description: + 'Notaður sem titill á tilkynningarglugga um að senda til fullnustu.', + }, + modalText: { + id: 'judicial.system.core:send_to_prison_admin.modal_text', + defaultMessage: + 'Mál {courtCaseNumber} verður sent til Fangelsismálastofnunar til fullnustu.\nÁkærði: {defendant}.', + description: + 'Notaður sem texti á tilkynningarglugga um að senda til fullnustu', + }, + modalNextButtonText: { + id: 'judicial.system.core:send_to_prison_admin.modal_next_button_text', + defaultMessage: 'Senda núna', + description: + 'Notaður sem texti í takka á tilkynningarglugga um að senda til fullnustu', + }, +}) diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.tsx new file mode 100644 index 000000000000..84ba3f0e18ad --- /dev/null +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.tsx @@ -0,0 +1,169 @@ +import { FC, useCallback, useContext, useState } from 'react' +import { useIntl } from 'react-intl' +import { useParams, useRouter } from 'next/navigation' + +import { Box, UploadFile } from '@island.is/island-ui/core' +import { PUBLIC_PROSECUTOR_STAFF_INDICTMENT_OVERVIEW_ROUTE } from '@island.is/judicial-system/consts' +import { core } from '@island.is/judicial-system-web/messages' +import { + CourtCaseInfo, + FormContentContainer, + FormContext, + FormFooter, + Modal, + PageHeader, + PageLayout, + PageTitle, + SectionHeading, +} from '@island.is/judicial-system-web/src/components' +import { CaseFileCategory } from '@island.is/judicial-system-web/src/graphql/schema' +import { + useDefendants, + useS3Upload, + useUploadFiles, +} from '@island.is/judicial-system-web/src/utils/hooks' + +import { strings } from './SendToPrisonAdmin.strings' + +enum AvailableModal { + SUCCESS = 'SUCCESS', +} + +const SendToPrisonAdmin: FC = () => { + const { workingCase, isLoadingWorkingCase, caseNotFound } = + useContext(FormContext) + const { formatMessage } = useIntl() + const [modalVisible, setModalVisible] = useState() + const [uploadFileError, setUploadFileError] = useState() + const router = useRouter() + const { defendantId } = useParams<{ caseId: string; defendantId: string }>() + const { handleUpload, handleRemove } = useS3Upload(workingCase.id) + const { updateDefendant, isUpdatingDefendant } = useDefendants() + const { uploadFiles, removeUploadFile, addUploadFiles, updateUploadFile } = + useUploadFiles() + + const defendant = workingCase.defendants?.find( + (defendant) => defendant.id === defendantId, + ) + + const handleNextButtonClick = useCallback(async () => { + setModalVisible(AvailableModal.SUCCESS) + }, []) + + const handleSecondaryButtonClick = () => { + setModalVisible(undefined) + } + + const handlePrimaryButtonClick = async () => { + if (!defendant) { + return + } + + await updateDefendant({ + defendantId: defendant.id, + caseId: workingCase.id, + isSentToPrisonAdmin: true, + }) + + // TODO: UNCOMMENT WHEN THIS FEATURE IS READY + // const uploadResult = await handleUpload( + // uploadFiles.filter((file) => file.percent === 0), + // updateUploadFile, + // ) + + // if (uploadResult !== 'ALL_SUCCEEDED') { + // setUploadFileError(formatMessage(errors.uploadFailed)) + // return + // } + + router.push( + `${PUBLIC_PROSECUTOR_STAFF_INDICTMENT_OVERVIEW_ROUTE}/${workingCase.id}`, + ) + } + + const handleFileUpload = useCallback( + (files: File[]) => { + addUploadFiles(files, { + category: CaseFileCategory.SENT_TO_PRISON_ADMIN_FILE, + status: 'done', + }) + }, + [addUploadFiles], + ) + + const handleRemoveFile = useCallback( + (file: UploadFile) => { + if (file.key) { + handleRemove(file, removeUploadFile) + } else { + removeUploadFile(file) + } + }, + [handleRemove, removeUploadFile], + ) + + return ( + + + + {formatMessage(strings.title)} + + + + {/* NOTE: This is temporarily disabled while we work on this + upload feature. + + file.category === CaseFileCategory.SENT_TO_PRISON_ADMIN_FILE, + )} + accept="application/pdf" + header={formatMessage(core.uploadBoxTitle)} + description={formatMessage(core.uploadBoxDescription, { + fileEndings: '.pdf', + })} + buttonLabel={formatMessage(core.uploadBoxButtonLabel)} + onChange={handleFileUpload} + onRemove={handleRemoveFile} + /> */} + + + + + + {modalVisible === AvailableModal.SUCCESS && defendant && ( + + )} + + ) +} + +export default SendToPrisonAdmin diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.strings.ts b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.strings.ts index 86650607956f..130b6c5b87da 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.strings.ts +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.strings.ts @@ -18,6 +18,12 @@ export const strings = defineMessages({ description: 'Notað sem texti á tagg fyrir "Unun" tillögu í yfirlesin mál málalista', }, + reviewTagFineAppealed: { + id: 'judicial.system.core:public_prosecutor.tables.cases_reviewed.review_tag_fine_appealed', + defaultMessage: 'Kært', + description: + 'Notað sem texti á tagg fyrir "Kært" tillögu í yfirlesin mál málalista', + }, infoContainerMessage: { id: 'judicial.system.core:public_prosecutor.tables.cases_reviewed.info_container_message', defaultMessage: 'Engin yfirlesin mál.', @@ -48,4 +54,10 @@ export const strings = defineMessages({ description: 'Notað sem texti á taggi fyrir "Dómur birtur" í yfirlesin mál málalista', }, + tagVerdictViewSentToPrisonAdmin: { + id: 'judicial.system.core:public_prosecutor.tables.cases_reviewed.tag_verdict_sent_to_prison_admin', + defaultMessage: 'Til fullnustu', + description: + 'Notað sem texti á taggi fyrir "Til fullnustu" í yfirlesin mál málalista', + }, }) diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.tsx index 6c483c7aedf2..3462a41a096e 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.tsx +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.tsx @@ -1,9 +1,10 @@ import { FC } from 'react' -import { useIntl } from 'react-intl' +import { MessageDescriptor, useIntl } from 'react-intl' import { AnimatePresence } from 'framer-motion' import { Tag, Text } from '@island.is/island-ui/core' import { capitalize } from '@island.is/judicial-system/formatters' +import { CaseIndictmentRulingDecision } from '@island.is/judicial-system/types' import { core, tables } from '@island.is/judicial-system-web/messages' import { SectionHeading } from '@island.is/judicial-system-web/src/components' import { useContextMenu } from '@island.is/judicial-system-web/src/components/ContextMenu/ContextMenu' @@ -31,26 +32,46 @@ const CasesReviewed: FC = ({ loading, cases }) => { const { formatMessage } = useIntl() const { openCaseInNewTabMenuItem } = useContextMenu() - const decisionMapping = { - [IndictmentCaseReviewDecision.ACCEPT]: formatMessage( - strings.reviewTagAccepted, - ), - [IndictmentCaseReviewDecision.APPEAL]: formatMessage( - strings.reviewTagAppealed, - ), + const indictmentReviewDecisionMapping = ( + reviewDecision: IndictmentCaseReviewDecision, + isFine: boolean, + ) => { + if (reviewDecision === IndictmentCaseReviewDecision.ACCEPT) { + return formatMessage(strings.reviewTagAccepted) + } else if (reviewDecision === IndictmentCaseReviewDecision.APPEAL) { + return formatMessage( + isFine ? strings.reviewTagFineAppealed : strings.reviewTagAppealed, + ) + } else { + return null + } } const getVerdictViewTag = (row: CaseListEntry) => { - const variant = !row.indictmentVerdictViewedByAll - ? 'red' - : row.indictmentVerdictAppealDeadlineExpired - ? 'mint' - : 'blue' - const message = !row.indictmentVerdictViewedByAll - ? strings.tagVerdictUnviewed - : row.indictmentVerdictAppealDeadlineExpired - ? strings.tagVerdictViewComplete - : strings.tagVerdictViewOnDeadline + let variant: 'red' | 'mint' | 'blue' + let message: MessageDescriptor + + const someDefendantIsSentToPrisonAdmin = Boolean( + row.defendants?.length && + row.defendants.some((defendant) => defendant.isSentToPrisonAdmin), + ) + + if (row.indictmentRulingDecision === CaseIndictmentRulingDecision.FINE) { + return null + } else if (someDefendantIsSentToPrisonAdmin) { + variant = 'red' + message = strings.tagVerdictViewSentToPrisonAdmin + } else if (!row.indictmentVerdictViewedByAll) { + variant = 'red' + message = strings.tagVerdictUnviewed + } else if (row.indictmentVerdictAppealDeadlineExpired) { + variant = 'mint' + message = strings.tagVerdictViewComplete + } else { + variant = 'blue' + message = strings.tagVerdictViewOnDeadline + } + return ( {formatMessage(message)} @@ -100,7 +121,11 @@ const CasesReviewed: FC = ({ loading, cases }) => { cell: (row) => ( {row.indictmentReviewDecision && - decisionMapping[row.indictmentReviewDecision]} + indictmentReviewDecisionMapping( + row.indictmentReviewDecision, + row.indictmentRulingDecision === + CaseIndictmentRulingDecision.FINE, + )} ), }, diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.css.ts b/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.css.ts index f3b85cfcbefe..f0211d02ca05 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.css.ts +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.css.ts @@ -4,7 +4,12 @@ import { theme } from '@island.is/island-ui/theme' export const gridRow = style({ display: 'grid', - gridTemplateColumns: '1.6fr 1fr', + gridTemplateColumns: '1fr auto', gridGap: theme.spacing[1], - marginBottom: theme.spacing[1], + + '@media': { + [`screen and (max-width: ${theme.breakpoints.lg}px)`]: { + gridTemplateColumns: '1fr', + }, + }, }) diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.strings.ts b/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.strings.ts index 796a7372f889..c9250aa3fd48 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.strings.ts +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.strings.ts @@ -2,14 +2,14 @@ import { defineMessages } from 'react-intl' export const strings = defineMessages({ title: { - id: 'judicial.system.core:public_prosecutor.indictments.review_decision.title', - defaultMessage: 'Ákvörðun um áfrýjun', + id: 'judicial.system.core:public_prosecutor.indictments.review_decision.title_v1', + defaultMessage: 'Ákvörðun um {isFine, select, true {kæru} other {áfrýjun}}', description: 'Notaður sem titill á ákvörðum um áfrýjun boxi fyrir ákæru.', }, subtitle: { - id: 'judicial.system.core:public_prosecutor.indictments.review_decision.subtitle', + id: 'judicial.system.core:public_prosecutor.indictments.review_decision.subtitle_v1', defaultMessage: - 'Frestur til að áfrýja dómi rennur út {indictmentAppealDeadline}', + 'Frestur til að {isFine, select, true {kæra viðurlagaákvörðun} other {áfrýja dómi}} {appealDeadlineIsInThePast, select, true {rann} other {rennur}} út {indictmentAppealDeadline}', description: 'Notaður sem undirtitill á ákvörðum um áfrýjun boxi fyrir ákæru.', }, @@ -24,6 +24,18 @@ export const strings = defineMessages({ defaultMessage: 'Una héraðsdómi', description: 'Notaður sem texti fyrir "Una héraðsdómi" radio takka.', }, + appealFineToCourtOfAppeals: { + id: 'judicial.system.core:public_prosecutor.indictments.review_decision.appeal_fine_to_court_of_appeals', + defaultMessage: 'Kæra viðurlagaákvörðun til Landsréttar', + description: + 'Notaður sem texti fyrir "Kæra viðurlagaákvörðun til Landsréttar" radio takka.', + }, + acceptFineDecision: { + id: 'judicial.system.core:public_prosecutor.indictments.review_decision.accept_fine_decision', + defaultMessage: 'Una viðurlagaákvörðun', + description: + 'Notaður sem texti fyrir "Kæra viðurlagaákvörðun" radio takka.', + }, reviewModalTitle: { id: 'judicial.system.core:indictments_review.title', defaultMessage: 'Staðfesta ákvörðun', @@ -31,11 +43,16 @@ export const strings = defineMessages({ }, reviewModalText: { id: 'judicial.system.core:indictments_review.modal_text', - defaultMessage: 'Ertu viss um að þú viljir {reviewerDecision, select, ACCEPT {una héraðsdómi} APPEAL {áfrýja héraðsdómi til Landsréttar} other {halda áfram}}?', description: 'Notaður sem texti í yfirlitsglugga um yfirlit ákæru.', }, + reviewModalTextFine: { + id: 'judicial.system.core:indictments_review.modal_text_fine', + defaultMessage: + 'Ertu viss um að þú viljir {reviewerDecision, select, ACCEPT {una viðurlagaákvörðun} APPEAL {kæra viðurlagaákvörðun til Landsréttar} other {halda áfram}}?', + description: 'Notaður sem texti í yfirlitsglugga um yfirlit ákæru.', + }, reviewModalPrimaryButtonText: { id: 'judicial.system.core:indictments_review.modal_primary_button_text', defaultMessage: 'Staðfesta', diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.tsx index ff25436bea93..119153159b66 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.tsx +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.tsx @@ -21,25 +21,28 @@ import * as styles from './ReviewDecision.css' interface Props { caseId: string indictmentAppealDeadline?: string + indictmentAppealDeadlineIsInThePast?: boolean modalVisible?: boolean setModalVisible: Dispatch> + isFine: boolean onSelect?: () => void } export const ReviewDecision: FC = (props) => { - const { user } = useContext(UserContext) - const router = useRouter() - const { formatMessage: fm } = useIntl() - const { updateCase } = useCase() - const { caseId, indictmentAppealDeadline, + indictmentAppealDeadlineIsInThePast, modalVisible, setModalVisible, + isFine, onSelect, } = props + const { user } = useContext(UserContext) + const router = useRouter() + const { formatMessage: fm } = useIntl() + const { updateCase } = useCase() const [indictmentReviewDecision, setIndictmentReviewDecision] = useState< IndictmentCaseReviewDecision | undefined >(undefined) @@ -58,11 +61,15 @@ export const ReviewDecision: FC = (props) => { const options = [ { - label: fm(strings.appealToCourtOfAppeals), + label: fm( + isFine + ? strings.appealFineToCourtOfAppeals + : strings.appealToCourtOfAppeals, + ), value: IndictmentCaseReviewDecision.APPEAL, }, { - label: fm(strings.acceptDecision), + label: fm(isFine ? strings.acceptFineDecision : strings.acceptDecision), value: IndictmentCaseReviewDecision.ACCEPT, }, ] @@ -74,11 +81,13 @@ export const ReviewDecision: FC = (props) => { return ( {fm(strings.subtitle, { + isFine, indictmentAppealDeadline: formatDate(indictmentAppealDeadline), + appealDeadlineIsInThePast: indictmentAppealDeadlineIsInThePast, })} } @@ -107,9 +116,12 @@ export const ReviewDecision: FC = (props) => { {modalVisible && ( setModalVisible(false)} 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 5b14a32bf26d..7e444f9327d4 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/ActiveCases.tsx +++ b/apps/judicial-system/web/src/routes/Shared/Cases/ActiveCases.tsx @@ -57,22 +57,20 @@ const ActiveCases: FC = (props) => { }, ]} data={cases} - generateContextMenuItems={(row) => { - return [ - openCaseInNewTabMenuItem(row.id), - ...(canDeleteCase(row) - ? [ - { - title: formatMessage(contextMenu.deleteCase), - onClick: () => { - onContextMenuDeleteClick(row.id) - }, - icon: 'trash', - } as ContextMenuItem, - ] - : []), - ] - }} + generateContextMenuItems={(row) => [ + openCaseInNewTabMenuItem(row.id), + ...(canDeleteCase(row) + ? [ + { + title: formatMessage(contextMenu.deleteCase), + onClick: () => { + onContextMenuDeleteClick(row.id) + }, + icon: 'trash', + } as ContextMenuItem, + ] + : []), + ]} columns={[ { cell: (row) => ( diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/Cases.spec.tsx b/apps/judicial-system/web/src/routes/Shared/Cases/Cases.spec.tsx index 5a95618ec575..ddd0eb6c926d 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/Cases.spec.tsx +++ b/apps/judicial-system/web/src/routes/Shared/Cases/Cases.spec.tsx @@ -1,6 +1,5 @@ import { MockedProvider } from '@apollo/client/testing' import { render, screen, waitFor } from '@testing-library/react' -import userEvent from '@testing-library/user-event' import { UserProvider } from '@island.is/judicial-system-web/src/components' import { @@ -302,112 +301,6 @@ describe('Cases', () => { }) }) - describe('All user types - sorting', () => { - test('should order the table data by accused name in ascending order when the user clicks the accused name table header', async () => { - const user = userEvent.setup() - render( - - - - - - - , - ) - - await user.click(await screen.findByTestId('defendantsSortButton')) - - const tableRows = await screen.findAllByTestId('tableRow') - - expect(tableRows[0]).toHaveTextContent('D. M. Kil') - expect(tableRows[1]).toHaveTextContent('Erlingur L Kristinsson') - expect(tableRows[2]).toHaveTextContent('Jon Harring') - expect(tableRows[3]).toHaveTextContent('Jon Harring Sr.') - expect(tableRows[4]).toHaveTextContent('Moe') - }) - - test('should order the table data by accused name in descending order when the user clicks the accused name table header twice', async () => { - const user = userEvent.setup() - render( - - - - - - - , - ) - - await user.dblClick(await screen.findByTestId('defendantsSortButton')) - - const tableRows = await screen.findAllByTestId('tableRow') - - expect(tableRows[0]).toHaveTextContent('D. M. Kil') - expect(tableRows[1]).toHaveTextContent('Erlingur L Kristinsson') - expect(tableRows[2]).toHaveTextContent('Jon Harring') - expect(tableRows[3]).toHaveTextContent('Jon Harring Sr.') - expect(tableRows[4]).toHaveTextContent('Moe') - }) - - test('should order the table data by created in ascending order when the user clicks the created table header', async () => { - const user = userEvent.setup() - render( - - - - - - - , - ) - - await user.click(await screen.findByTestId('createdSortButton')) - - const tableRows = await screen.findAllByTestId('tableRow') - - expect(tableRows[0]).toHaveTextContent('Erlingur L Kristinsson') - expect(tableRows[1]).toHaveTextContent('Jon Harring Sr.') - expect(tableRows[2]).toHaveTextContent('Jon Harring') - expect(tableRows[3]).toHaveTextContent('D. M. Kil') - expect(tableRows[4]).toHaveTextContent('Moe') - }) - - test('should order the table data by created in acending order when the user clicks the created table header twice', async () => { - const user = userEvent.setup() - render( - - - - - - - , - ) - - await user.dblClick(await screen.findByTestId('createdSortButton')) - - const tableRows = await screen.findAllByTestId('tableRow') - - expect(tableRows[0]).toHaveTextContent('Erlingur L Kristinsson') - expect(tableRows[1]).toHaveTextContent('Jon Harring Sr.') - expect(tableRows[2]).toHaveTextContent('Jon Harring') - expect(tableRows[3]).toHaveTextContent('D. M. Kil') - expect(tableRows[4]).toHaveTextContent('Moe') - }) - }) - describe('All user types - error handling', () => { test('should display an error alert if the api call fails', async () => { render( diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx b/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx index a2c1ecb6e872..cf4526f8d3af 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx +++ b/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx @@ -347,11 +347,7 @@ export const Cases: FC = () => { )} {loading || pastCases.length > 0 ? ( - + ) : (
{ indictmentAppealDeadline={ workingCase.indictmentAppealDeadline ?? '' } + indictmentAppealDeadlineIsInThePast={ + workingCase.indictmentVerdictAppealDeadlineExpired ?? false + } modalVisible={modalVisible} setModalVisible={setModalVisible} + isFine={ + workingCase.indictmentRulingDecision === + CaseIndictmentRulingDecision.FINE + } onSelect={() => setIsReviewDecisionSelected(true)} /> )} diff --git a/apps/judicial-system/web/src/routes/Shared/Login/Login.tsx b/apps/judicial-system/web/src/routes/Shared/Login/Login.tsx index cd4f118ceb05..70134b735f24 100644 --- a/apps/judicial-system/web/src/routes/Shared/Login/Login.tsx +++ b/apps/judicial-system/web/src/routes/Shared/Login/Login.tsx @@ -1,12 +1,27 @@ +import { useContext, useEffect } from 'react' import { useIntl } from 'react-intl' import { useRouter } from 'next/router' import { AlertMessage, Box, Button, Text } from '@island.is/island-ui/core' +import { + CASES_ROUTE, + COURT_OF_APPEAL_CASES_ROUTE, + DEFENDER_CASES_ROUTE, + PRISON_CASES_ROUTE, + USERS_ROUTE, +} from '@island.is/judicial-system/consts' +import { + isCourtOfAppealsUser, + isDefenceUser, + isPrisonStaffUser, +} from '@island.is/judicial-system/types' import { login, titles } from '@island.is/judicial-system-web/messages' import { PageHeader, PageTitle, + UserContext, } from '@island.is/judicial-system-web/src/components' +import { UserRole } from '@island.is/judicial-system-web/src/graphql/schema' import { api } from '@island.is/judicial-system-web/src/services' import { LoginErrorCodes } from '@island.is/judicial-system-web/src/types' @@ -15,6 +30,7 @@ import * as styles from './Login.css' const Login = () => { const router = useRouter() const { formatMessage } = useIntl() + const { user } = useContext(UserContext) const getErrorAlert = (errorCode: LoginErrorCodes): JSX.Element | null => { switch (errorCode) { @@ -55,6 +71,22 @@ const Login = () => { } } + useEffect(() => { + if (user) { + const redirectRoute = isDefenceUser(user) + ? DEFENDER_CASES_ROUTE + : isPrisonStaffUser(user) + ? PRISON_CASES_ROUTE + : isCourtOfAppealsUser(user) + ? COURT_OF_APPEAL_CASES_ROUTE + : user.role === UserRole.ADMIN + ? USERS_ROUTE + : CASES_ROUTE + + router.push(redirectRoute) + } + }, [router, user]) + return ( <> diff --git a/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts b/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts index 66987e7b513b..49da94987d47 100644 --- a/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts @@ -20,7 +20,8 @@ const useDefendants = () => { const [createDefendantMutation, { loading: isCreatingDefendant }] = useCreateDefendantMutation() const [deleteDefendantMutation] = useDeleteDefendantMutation() - const [updateDefendantMutation] = useUpdateDefendantMutation() + const [updateDefendantMutation, { loading: isUpdatingDefendant }] = + useUpdateDefendantMutation() const createDefendant = useCallback( async (defendant: CreateDefendantInput) => { @@ -118,6 +119,7 @@ const useDefendants = () => { createDefendant, deleteDefendant, updateDefendant, + isUpdatingDefendant, updateDefendantState, setAndSendDefendantToServer, } diff --git a/apps/judicial-system/web/src/utils/hooks/useEventLog/index.ts b/apps/judicial-system/web/src/utils/hooks/useEventLog/index.ts index d8a89c385f64..ade728d7ccae 100644 --- a/apps/judicial-system/web/src/utils/hooks/useEventLog/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useEventLog/index.ts @@ -29,6 +29,8 @@ const useEventLog = () => { [createEventLogMutation, formatMessage], ) - return { createEventLog } + return { + createEventLog, + } } export default useEventLog diff --git a/apps/judicial-system/web/src/utils/validate.ts b/apps/judicial-system/web/src/utils/validate.ts index aaff6d5c197b..390946736f4a 100644 --- a/apps/judicial-system/web/src/utils/validate.ts +++ b/apps/judicial-system/web/src/utils/validate.ts @@ -262,10 +262,9 @@ export const isHearingArrangementsStepValidIC = ( export const isProcessingStepValidIndictments = ( workingCase: Case, ): boolean => { - const defendantsAreValid = () => - workingCase.defendants?.every((defendant) => { - return validate([[defendant.defendantPlea, ['empty']]]).isValid - }) + const defendantsAreValid = workingCase.defendants?.every( + (defendant) => validate([[defendant.defendantPlea, ['empty']]]).isValid, + ) const hasCivilClaimSelected = workingCase.hasCivilClaims !== null && @@ -275,9 +274,14 @@ export const isProcessingStepValidIndictments = ( ? workingCase.civilClaimants?.every( (civilClaimant) => civilClaimant.name && - (civilClaimant.noNationalId || - (civilClaimant.nationalId && - civilClaimant.nationalId.replace('-', '').length === 10)), + validate([ + [ + civilClaimant.nationalId, + civilClaimant.noNationalId + ? ['date-of-birth'] + : ['empty', 'national-id'], + ], + ]).isValid, ) : true @@ -286,7 +290,7 @@ export const isProcessingStepValidIndictments = ( workingCase.court && hasCivilClaimSelected && allCivilClaimantsAreValid && - defendantsAreValid(), + defendantsAreValid, ) } diff --git a/apps/native/app/src/graphql/queries/health.graphql b/apps/native/app/src/graphql/queries/health.graphql index d0a5d93185da..ee4446c8f783 100644 --- a/apps/native/app/src/graphql/queries/health.graphql +++ b/apps/native/app/src/graphql/queries/health.graphql @@ -77,3 +77,19 @@ query GetVaccinations($locale: String) { } } } + +query GetOrganDonorStatus($locale: String) { + healthDirectorateOrganDonation(locale: $locale) { + donor { + isDonor + limitations { + hasLimitations + limitedOrgansList { + id + name + } + comment + } + } + } +} diff --git a/apps/native/app/src/graphql/queries/inbox.graphql b/apps/native/app/src/graphql/queries/inbox.graphql index 73209b4263fc..ff129a3e424a 100644 --- a/apps/native/app/src/graphql/queries/inbox.graphql +++ b/apps/native/app/src/graphql/queries/inbox.graphql @@ -48,3 +48,10 @@ mutation PostMailActionMutation($input: DocumentsV2MailActionInput!) { success } } + +query DocumentConfirmActions($input: DocumentConfirmActionsInput!) { + documentV2ConfirmActions(input: $input) { + id + confirmed + } +} diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts index 734168b2c9bb..24a6e3413d63 100644 --- a/apps/native/app/src/messages/en.ts +++ b/apps/native/app/src/messages/en.ts @@ -594,6 +594,9 @@ export const en: TranslatedMessages = { 'problem.offline.title': 'No internet connection', 'problem.offline.message': 'An error occurred while communicating with the service provider', + 'problem.thirdParty.title': 'Service unreachable', + 'problem.thirdParty.message': + 'An error occurred while communicating with the service provider', // passkeys 'passkeys.headingTitle': 'Sign in with Island.is app', @@ -659,4 +662,18 @@ export const en: TranslatedMessages = { 'health.vaccinations.noVaccinationsDescription': 'If you believe you have data that should appear here, please contact service provider.', 'health.vaccinations.directorateOfHealth': 'The directorate of Health', + + // health - organ donation + 'health.organDonation': 'Organ Donation', + 'health.organDonation.change': 'Breyta afstöðu', + 'health.organDonation.isDonor': 'Ég er líffæragjafi', + 'health.organDonation.isDonorWithLimitations': + 'Ég heimila líffæragjöf, með takmörkunum.', + 'health.organDonation.isNotDonor': 'Ég heimila ekki líffæragjöf', + 'health.organDonation.isDonorDescription': + 'Öll líffærin mín má nota til ígræðslu.', + 'health.organDonation.isNotDonorDescription': + 'Engin líffæri má nota til ígræðslu.', + 'health.organDonation.isDonorWithLimitationsDescription': + 'Öll líffærin mín má nota til ígræðslu fyrir utan: {limitations}.', } diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts index 0135ba4a659e..a0ee501f24b1 100644 --- a/apps/native/app/src/messages/is.ts +++ b/apps/native/app/src/messages/is.ts @@ -595,6 +595,8 @@ export const is = { 'Ef þú telur þig eiga gögn sem ættu að birtast hér, vinsamlegast hafðu samband við þjónustuaðila.', 'problem.offline.title': 'Samband næst ekki', 'problem.offline.message': 'Villa kom upp í samskiptum við þjónustuaðila', + 'problem.thirdParty.title': 'Samband næst ekki', + 'problem.thirdParty.message': 'Villa kom upp í samskiptum við þjónustuaðila', // passkeys 'passkeys.headingTitle': 'Innskrá með Ísland.is appinu', @@ -661,4 +663,18 @@ export const is = { 'health.vaccinations.noVaccinationsDescription': 'Ef þú telur þig eiga gögn sem ættu að birtast hér, vinsamlegast hafðu samband við þjónustuaðila.', 'health.vaccinations.directorateOfHealth': 'Embætti landlæknis', + + // health - organ donation + 'health.organDonation': 'Líffæragjöf', + 'health.organDonation.change': 'Breyta afstöðu', + 'health.organDonation.isDonor': 'Ég er líffæragjafi', + 'health.organDonation.isDonorWithLimitations': + 'Ég heimila líffæragjöf, með takmörkunum.', + 'health.organDonation.isNotDonor': 'Ég heimila ekki líffæragjöf', + 'health.organDonation.isDonorDescription': + 'Öll líffærin mín má nota til ígræðslu.', + 'health.organDonation.isNotDonorDescription': + 'Engin líffæri má nota til ígræðslu.', + 'health.organDonation.isDonorWithLimitationsDescription': + 'Öll líffærin mín má nota til ígræðslu fyrir utan: {limitations}.', } diff --git a/apps/native/app/src/screens/document-detail/document-detail.tsx b/apps/native/app/src/screens/document-detail/document-detail.tsx index e70d6f884bfc..9914b0937f78 100644 --- a/apps/native/app/src/screens/document-detail/document-detail.tsx +++ b/apps/native/app/src/screens/document-detail/document-detail.tsx @@ -27,6 +27,7 @@ import { DocumentV2Action, ListDocumentFragmentDoc, useGetDocumentQuery, + useDocumentConfirmActionsLazyQuery, } from '../../graphql/types/schema' import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator' @@ -248,6 +249,17 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{ const [pdfUrl, setPdfUrl] = useState('') const [refetching, setRefetching] = useState(false) + const [logConfirmedAction] = useDocumentConfirmActionsLazyQuery({ + fetchPolicy: 'no-cache', + }) + + const confirmAction = async (confirmed: boolean) => { + // Adding a suffix '_app' to the id since the backend is currently not distinguishing between the app and the web + await logConfirmedAction({ + variables: { input: { id: `${docId}_app`, confirmed: confirmed } }, + }) + } + const refetchDocumentContent = async () => { setRefetching(true) try { @@ -268,11 +280,17 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{ { text: intl.formatMessage({ id: 'inbox.markAllAsReadPromptCancel' }), style: 'cancel', - onPress: () => Navigation.pop(componentId), + onPress: async () => { + await confirmAction(false) + Navigation.pop(componentId) + }, }, { text: intl.formatMessage({ id: 'inbox.openDocument' }), - onPress: refetchDocumentContent, + onPress: async () => { + await confirmAction(true) + await refetchDocumentContent() + }, }, ]) } diff --git a/apps/native/app/src/screens/health/health-overview.tsx b/apps/native/app/src/screens/health/health-overview.tsx index 25fce0d06d70..740cac1bbb45 100644 --- a/apps/native/app/src/screens/health/health-overview.tsx +++ b/apps/native/app/src/screens/health/health-overview.tsx @@ -1,4 +1,12 @@ -import { Alert, Button, Heading, Input, InputRow, Typography } from '@ui' +import { + Alert, + Button, + Heading, + Input, + InputRow, + Problem, + Typography, +} from '@ui' import React, { useCallback, useMemo, useState } from 'react' import { FormattedMessage, useIntl } from 'react-intl' import { @@ -12,11 +20,13 @@ import { } from 'react-native' import { NavigationFunctionComponent } from 'react-native-navigation' import styled, { useTheme } from 'styled-components/native' +import { ApolloError } from '@apollo/client' import { useGetHealthCenterQuery, useGetHealthInsuranceOverviewQuery, useGetMedicineDataQuery, + useGetOrganDonorStatusQuery, useGetPaymentOverviewQuery, useGetPaymentStatusQuery, } from '../../graphql/types/schema' @@ -27,6 +37,7 @@ import { useBrowser } from '../../lib/use-browser' import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator' import { navigateTo } from '../../lib/deep-linking' import { useFeatureFlag } from '../../contexts/feature-flag-provider' +import { useLocale } from '../../hooks/use-locale' const Host = styled(SafeAreaView)` padding-horizontal: ${({ theme }) => theme.spacing[2]}px; @@ -43,10 +54,15 @@ const ButtonWrapper = styled.View` interface HeadingSectionProps { title: string + linkTextId?: string onPress: () => void } -const HeadingSection: React.FC = ({ title, onPress }) => { +const HeadingSection: React.FC = ({ + title, + onPress, + linkTextId, +}) => { const theme = useTheme() return ( @@ -65,7 +81,7 @@ const HeadingSection: React.FC = ({ title, onPress }) => { color={theme.color.blue400} style={{ marginRight: 4 }} > - + @@ -77,6 +93,10 @@ const HeadingSection: React.FC = ({ title, onPress }) => { ) } +const showErrorComponent = (error: ApolloError) => { + return +} + const { getNavigationOptions, useNavigationOptions } = createNavigationOptionHooks((theme, intl) => ({ topBar: { @@ -98,10 +118,17 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ const { width } = useWindowDimensions() const buttonStyle = { flex: 1, minWidth: width * 0.5 - theme.spacing[3] } const isVaccinationsEnabled = useFeatureFlag('isVaccinationsEnabled', false) + const isOrganDonationEnabled = useFeatureFlag('isOrganDonationEnabled', false) const now = useMemo(() => new Date().toISOString(), []) const medicinePurchaseRes = useGetMedicineDataQuery() + const organDonationRes = useGetOrganDonorStatusQuery({ + variables: { + locale: useLocale(), + }, + skip: !isOrganDonationEnabled, + }) const healthInsuranceRes = useGetHealthInsuranceOverviewQuery() const healthCenterRes = useGetHealthCenterQuery() const paymentStatusRes = useGetPaymentStatusQuery() @@ -124,12 +151,25 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ const paymentStatusData = paymentStatusRes.data?.rightsPortalCopaymentStatus const paymentOverviewData = paymentOverviewRes.data?.rightsPortalPaymentOverview?.items?.[0] + const organDonationData = + organDonationRes.data?.healthDirectorateOrganDonation.donor const isMedicinePeriodActive = medicinePurchaseData?.active || (medicinePurchaseData?.dateTo && new Date(medicinePurchaseData.dateTo) > new Date()) + const isOrganDonor = organDonationData?.isDonor ?? false + + const isOrganDonorWithLimitations = + isOrganDonor && (organDonationData?.limitations?.hasLimitations ?? false) + + const organLimitations = isOrganDonorWithLimitations + ? organDonationData?.limitations?.limitedOrgansList?.map( + (organ) => organ.name, + ) ?? [] + : [] + useConnectivityIndicator({ componentId, refetching, @@ -152,7 +192,8 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ healthCenterRes.refetch(), paymentStatusRes.refetch(), paymentOverviewRes.refetch(), - ] + isOrganDonationEnabled && organDonationRes.refetch(), + ].filter(Boolean) await Promise.all(promises) } catch (e) { // noop @@ -165,6 +206,8 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ healthCenterRes, paymentStatusRes, paymentOverviewRes, + organDonationRes, + isOrganDonationEnabled, ]) return ( @@ -247,49 +290,57 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ ) } /> - - - - - - + {(healthCenterRes.data || healthCenterRes.loading) && ( + <> + + + + + + + + )} + {healthCenterRes.error && + !healthCenterRes.data && + showErrorComponent(healthCenterRes.error)} openBrowser(`${origin}/minarsidur/heilsa/yfirlit`, componentId) } /> - {healthInsuranceData?.isInsured || healthInsuranceRes.loading ? ( + {(healthInsuranceRes.data && healthInsuranceData?.isInsured) || + healthInsuranceRes.loading ? ( ) : ( - + !healthInsuranceRes.error && + healthInsuranceRes.data && ( + + ) )} + {healthInsuranceRes.error && + !healthInsuranceRes.data && + showErrorComponent(healthInsuranceRes.error)} - - - - - - - - - - + {(paymentOverviewRes.loading || paymentOverviewRes.data) && ( + <> + + + + + + + + + + + + )} + {paymentOverviewRes.error && + !paymentOverviewRes.data && + paymentStatusRes.error && + !paymentStatusRes.data && + showErrorComponent(paymentOverviewRes.error)} - - - - - + + + + + + + + )} + {medicinePurchaseRes.error && + !medicinePurchaseRes.data && + showErrorComponent(medicinePurchaseRes.error)} + {isOrganDonationEnabled && ( + + openBrowser( + `${origin}/minarsidur/heilsa/liffaeragjof/skraning`, + componentId, + ) } /> - + )} + {isOrganDonationEnabled && ( + + + + )} + {isOrganDonationEnabled && + organDonationRes.error && + !organDonationRes.data && + showErrorComponent(organDonationRes.error)} diff --git a/apps/native/app/src/stores/organizations-store.ts b/apps/native/app/src/stores/organizations-store.ts index 2d3580092443..3b9cb837e73a 100644 --- a/apps/native/app/src/stores/organizations-store.ts +++ b/apps/native/app/src/stores/organizations-store.ts @@ -30,6 +30,7 @@ interface Organization { interface OrganizationsStore extends State { organizations: Organization[] getOrganizationLogoUrl(forName: string, size?: number): ImageSourcePropType + getOrganizationNameBySlug(slug: string): string actions: any } @@ -72,6 +73,10 @@ export const organizationsStore = create( const uri = `${url}?w=${size}&h=${size}&fit=pad&fm=png` return { uri } }, + getOrganizationNameBySlug(slug: string) { + const org = get().organizations.find((o) => o.slug === slug) + return org?.title ?? '' + }, actions: { updateOriganizations: async () => { const client = await getApolloClientAsync() diff --git a/apps/native/app/src/ui/lib/problem/problem-template.tsx b/apps/native/app/src/ui/lib/problem/problem-template.tsx index b15358fbdc05..1ebe261c581f 100644 --- a/apps/native/app/src/ui/lib/problem/problem-template.tsx +++ b/apps/native/app/src/ui/lib/problem/problem-template.tsx @@ -10,6 +10,7 @@ export type ProblemTemplateBaseProps = { title: string message: string | ReactNode withContainer?: boolean + size?: 'small' | 'large' } interface WithIconProps extends ProblemTemplateBaseProps { @@ -68,6 +69,7 @@ const getColorsByVariant = ( const Host = styled.View<{ borderColor: Colors noContainer?: boolean + size: 'small' | 'large' }>` border-color: ${({ borderColor, theme }) => theme.color[borderColor]}; border-width: 1px; @@ -76,11 +78,12 @@ const Host = styled.View<{ justify-content: center; align-items: center; flex: 1; - row-gap: ${({ theme }) => theme.spacing[3]}px; + row-gap: ${({ theme, size }) => + size === 'small' ? theme.spacing[2] : theme.spacing[3]}px; padding: ${({ theme }) => theme.spacing[2]}px; ${({ noContainer, theme }) => noContainer && `margin: ${theme.spacing[2]}px;`} - min-height: 280px; + min-height: ${({ size }) => (size === 'large' ? '280' : '142')}px; ` const Tag = styled(View)<{ @@ -114,12 +117,13 @@ export const ProblemTemplate = ({ showIcon, tag, withContainer, + size = 'large', }: ProblemTemplateProps) => { const { borderColor, tagColor, tagBackgroundColor } = getColorsByVariant(variant) return ( - + {tag && ( @@ -129,10 +133,18 @@ export const ProblemTemplate = ({ )} {showIcon && } - + {title} - {message} + + {message} + ) diff --git a/apps/native/app/src/ui/lib/problem/problem.tsx b/apps/native/app/src/ui/lib/problem/problem.tsx index 40963f69836c..4fa3d4e984d1 100644 --- a/apps/native/app/src/ui/lib/problem/problem.tsx +++ b/apps/native/app/src/ui/lib/problem/problem.tsx @@ -1,7 +1,10 @@ import { useEffect } from 'react' + import { useTranslate } from '../../../hooks/use-translate' import { useOfflineStore } from '../../../stores/offline-store' import { ProblemTemplate, ProblemTemplateBaseProps } from './problem-template' +import { getOrganizationSlugFromError } from '../../../utils/get-organization-slug-from-error' +import { ThirdPartyServiceError } from './third-party-service-error' enum ProblemTypes { error = 'error', @@ -20,7 +23,7 @@ type ProblemBaseProps = { title?: string message?: string logError?: boolean -} & Pick +} & Pick interface ErrorProps extends ProblemBaseProps { type?: 'error' @@ -61,6 +64,7 @@ export const Problem = ({ logError = false, withContainer, showIcon, + size = 'large', }: ProblemProps) => { const t = useTranslate() const { isConnected } = useOfflineStore() @@ -73,6 +77,7 @@ export const Problem = ({ message: message ?? t('problem.error.message'), tag: tag ?? t('problem.error.tag'), variant: 'error', + size: size ?? 'large', } as const useEffect(() => { @@ -90,6 +95,7 @@ export const Problem = ({ variant="warning" title={title ?? t('problem.offline.title')} message={message ?? t('problem.offline.message')} + size={size} /> ) } @@ -99,6 +105,18 @@ export const Problem = ({ switch (type) { case ProblemTypes.error: + if (error) { + const organizationSlug = getOrganizationSlugFromError(error) + + if (organizationSlug) { + return ( + + ) + } + } return case ProblemTypes.noData: @@ -109,6 +127,7 @@ export const Problem = ({ variant="info" title={title ?? t('problem.noData.title')} message={message ?? t('problem.noData.message')} + size={size} /> ) diff --git a/apps/native/app/src/ui/lib/problem/third-party-service-error.tsx b/apps/native/app/src/ui/lib/problem/third-party-service-error.tsx new file mode 100644 index 000000000000..d2a714c18694 --- /dev/null +++ b/apps/native/app/src/ui/lib/problem/third-party-service-error.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { useIntl } from 'react-intl' + +import { ProblemTemplate } from './problem-template' +import { useOrganizationsStore } from '../../../stores/organizations-store' + +type ThirdPartyServiceErrorProps = { + organizationSlug: string + size: 'small' | 'large' +} + +export const ThirdPartyServiceError = ({ + organizationSlug, + size, +}: ThirdPartyServiceErrorProps) => { + const intl = useIntl() + + const { getOrganizationNameBySlug } = useOrganizationsStore() + const organizationName = getOrganizationNameBySlug(organizationSlug) + + return ( + + ) +} diff --git a/apps/native/app/src/utils/get-organization-slug-from-error.ts b/apps/native/app/src/utils/get-organization-slug-from-error.ts new file mode 100644 index 000000000000..1463c87cb8db --- /dev/null +++ b/apps/native/app/src/utils/get-organization-slug-from-error.ts @@ -0,0 +1,36 @@ +import { ApolloError } from '@apollo/client' + +type PartialProblem = { + organizationSlug?: string +} + +type CustomExtension = { + code: string + problem?: PartialProblem + exception?: { + problem?: PartialProblem + } +} + +/** + * Extracts the organization slug from the Apollo error, if it exists. + */ +export const getOrganizationSlugFromError = (error: ApolloError | unknown) => { + const graphQLErrors = (error as ApolloError)?.graphQLErrors + + if (graphQLErrors) { + for (const graphQLError of graphQLErrors) { + const extensions = graphQLError.extensions as CustomExtension + + const organizationSlug = + extensions?.problem?.organizationSlug ?? + extensions?.exception?.problem?.organizationSlug + + if (organizationSlug) { + return organizationSlug + } + } + } + + return undefined +} diff --git a/apps/native/app/src/utils/lifecycle/setup-globals.ts b/apps/native/app/src/utils/lifecycle/setup-globals.ts index a7cc08f279ff..ece34480d3c5 100644 --- a/apps/native/app/src/utils/lifecycle/setup-globals.ts +++ b/apps/native/app/src/utils/lifecycle/setup-globals.ts @@ -98,13 +98,12 @@ if (__DEV__) { getConfig().datadog ?? '', 'production', '2736367a-a841-492d-adef-6f5a509d6ec2', - true, // track User interactions (e.g.: Tap on buttons. You can use 'accessibilityLabel' element property to give tap action the name, otherwise element type will be reported) + false, // do not track User interactions (e.g.: Tap on buttons.) true, // track XHR Resources true, // track Errors ) ddconfig.nativeCrashReportEnabled = true - ddconfig.nativeViewTracking = true ddconfig.site = 'EU' ddconfig.serviceName = 'mobile-app' diff --git a/apps/portals/my-pages/src/components/Layout/SidebarLayout.tsx b/apps/portals/my-pages/src/components/Layout/SidebarLayout.tsx index 876beb20d79b..c9d16d216134 100644 --- a/apps/portals/my-pages/src/components/Layout/SidebarLayout.tsx +++ b/apps/portals/my-pages/src/components/Layout/SidebarLayout.tsx @@ -41,7 +41,7 @@ export const SidebarLayout: FC = ({ offset={['0', '0', '0', '0', '1/9']} span={['9/9', '9/9', '9/9', '9/9', '8/9']} > - {children} + {children} diff --git a/apps/services/auth/admin-api/infra/auth-admin-api.ts b/apps/services/auth/admin-api/infra/auth-admin-api.ts index 0fc25a62d5d4..cf773f0dbd94 100644 --- a/apps/services/auth/admin-api/infra/auth-admin-api.ts +++ b/apps/services/auth/admin-api/infra/auth-admin-api.ts @@ -4,7 +4,12 @@ import { service, ServiceBuilder, } from '../../../../../infra/src/dsl/dsl' -import { Base, Client, RskProcuring } from '../../../../../infra/src/dsl/xroad' +import { + Base, + Client, + NationalRegistryAuthB2C, + RskProcuring, +} from '../../../../../infra/src/dsl/xroad' const REDIS_NODE_CONFIG = { dev: json([ @@ -84,8 +89,10 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => { '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET', SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME', SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD', + NATIONAL_REGISTRY_B2C_CLIENT_SECRET: + '/k8s/services-auth/NATIONAL_REGISTRY_B2C_CLIENT_SECRET', }) - .xroad(Base, Client, RskProcuring) + .xroad(Base, Client, RskProcuring, NationalRegistryAuthB2C) .ingress({ primary: { host: { diff --git a/apps/services/auth/admin-api/project.json b/apps/services/auth/admin-api/project.json index ed4e11cf7369..2866faa0019a 100644 --- a/apps/services/auth/admin-api/project.json +++ b/apps/services/auth/admin-api/project.json @@ -42,6 +42,12 @@ }, "outputs": ["{workspaceRoot}/coverage/apps/services/auth/admin-api"] }, + "dev": { + "executor": "nx:run-commands", + "options": { + "command": "yarn start --project services-auth-admin-api" + } + }, "codegen/backend-schema": { "executor": "nx:run-commands", "options": { diff --git a/apps/services/auth/admin-api/src/app/app.module.ts b/apps/services/auth/admin-api/src/app/app.module.ts index dc56c6fe0343..5f947672c654 100644 --- a/apps/services/auth/admin-api/src/app/app.module.ts +++ b/apps/services/auth/admin-api/src/app/app.module.ts @@ -9,8 +9,10 @@ import { import { AuthModule } from '@island.is/auth-nest-tools' import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientConfig } from '@island.is/clients/national-registry-v3' import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' import { SyslumennClientConfig } from '@island.is/clients/syslumenn' +import { ZendeskServiceConfig } from '@island.is/clients/zendesk' import { AuditModule } from '@island.is/nest/audit' import { IdsClientConfig, XRoadConfig } from '@island.is/nest/config' import { FeatureFlagConfig } from '@island.is/nest/feature-flags' @@ -31,7 +33,6 @@ import { ProvidersModule } from './v2/providers/providers.module' import { ScopesModule } from './v2/scopes/scopes.module' import { ClientSecretsModule } from './v2/secrets/client-secrets.module' import { TenantsModule } from './v2/tenants/tenants.module' -import { ZendeskServiceConfig } from '@island.is/clients/zendesk' @Module({ imports: [ @@ -61,6 +62,7 @@ import { ZendeskServiceConfig } from '@island.is/clients/zendesk' DelegationConfig, RskRelationshipsClientConfig, NationalRegistryClientConfig, + NationalRegistryV3ClientConfig, CompanyRegistryConfig, FeatureFlagConfig, XRoadConfig, diff --git a/apps/services/auth/delegation-api/infra/delegation-api.ts b/apps/services/auth/delegation-api/infra/delegation-api.ts index ea9252cca1f1..1dc2615d28d6 100644 --- a/apps/services/auth/delegation-api/infra/delegation-api.ts +++ b/apps/services/auth/delegation-api/infra/delegation-api.ts @@ -4,7 +4,12 @@ import { service, ServiceBuilder, } from '../../../../../infra/src/dsl/dsl' -import { Base, Client, RskProcuring } from '../../../../../infra/src/dsl/xroad' +import { + Base, + Client, + NationalRegistryAuthB2C, + RskProcuring, +} from '../../../../../infra/src/dsl/xroad' const REDIS_NODE_CONFIG = { dev: json([ @@ -77,8 +82,10 @@ export const serviceSetup = (services: { '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET', SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME', SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD', + NATIONAL_REGISTRY_B2C_CLIENT_SECRET: + '/k8s/services-auth/NATIONAL_REGISTRY_B2C_CLIENT_SECRET', }) - .xroad(Base, Client, RskProcuring) + .xroad(Base, Client, RskProcuring, NationalRegistryAuthB2C) .readiness('/health/check') .liveness('/liveness') .replicaCount({ diff --git a/apps/services/auth/delegation-api/src/app/app.module.ts b/apps/services/auth/delegation-api/src/app/app.module.ts index ac10af9d95f1..71b347827378 100644 --- a/apps/services/auth/delegation-api/src/app/app.module.ts +++ b/apps/services/auth/delegation-api/src/app/app.module.ts @@ -9,6 +9,7 @@ import { import { AuthModule } from '@island.is/auth-nest-tools' import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientConfig } from '@island.is/clients/national-registry-v3' import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' import { SyslumennClientConfig } from '@island.is/clients/syslumenn' import { AuditModule } from '@island.is/nest/audit' @@ -48,6 +49,7 @@ import { ScopesModule } from './scopes/scopes.module' FeatureFlagConfig, IdsClientConfig, NationalRegistryClientConfig, + NationalRegistryV3ClientConfig, RskRelationshipsClientConfig, CompanyRegistryConfig, XRoadConfig, diff --git a/apps/services/auth/delegation-api/src/app/delegations/test/me-delegations.access-incoming.spec.ts b/apps/services/auth/delegation-api/src/app/delegations/test/me-delegations.access-incoming.spec.ts index 81eea480ae5b..377a7771410b 100644 --- a/apps/services/auth/delegation-api/src/app/delegations/test/me-delegations.access-incoming.spec.ts +++ b/apps/services/auth/delegation-api/src/app/delegations/test/me-delegations.access-incoming.spec.ts @@ -8,8 +8,12 @@ import { Delegation, DelegationScope, DelegationsIndexService, + NationalRegistryV3FeatureService, } from '@island.is/auth-api-lib' +import { RskRelationshipsClient } from '@island.is/clients-rsk-relationships' import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientService } from '@island.is/clients/national-registry-v3' +import { CompanyRegistryClientService } from '@island.is/clients/rsk/company-registry' import { expectMatchingDelegations, FixtureFactory, @@ -21,176 +25,219 @@ import { accessIncomingTestCases } from '../../../../test/access-incoming-test-c import { setupWithAuth } from '../../../../test/setup' import { filterExpectedDelegations } from './utils' +const fromName: string = faker.name.findName() + describe('MeDelegationsController', () => { - describe.each(Object.keys(accessIncomingTestCases))( - 'Incoming Access with test case: %s', - (caseName) => { - const testCase = accessIncomingTestCases[caseName] - let app: TestApp - let server: request.SuperTest - let factory: FixtureFactory - let delegations: Delegation[] = [] - const fromName = faker.name.findName() - let delegationIndexService: DelegationsIndexService - - beforeAll(async () => { - // Arrange - app = await setupWithAuth({ - user: testCase.user, - customScopeRules: testCase.customScopeRules, - }) - server = request(app.getHttpServer()) - delegationIndexService = app.get(DelegationsIndexService) - const nationalRegistryClientService = app.get( - NationalRegistryClientService, - ) - jest - .spyOn(nationalRegistryClientService, 'getIndividual') - .mockImplementation(async (nationalId: string) => - createNationalRegistryUser({ - nationalId, - name: fromName, - }), - ) - jest - .spyOn(delegationIndexService, 'indexDelegations') - .mockImplementation() - jest - .spyOn(delegationIndexService, 'indexCustomDelegations') - .mockImplementation() - - factory = new FixtureFactory(app) - await Promise.all( - testCase.domains.map((domain) => factory.createDomain(domain)), - ) - await Promise.all( - (testCase.accessTo ?? []).map((scope) => - factory.createApiScopeUserAccess({ - nationalId: testCase.user.nationalId, - scope, - }), - ), - ) - }) - - beforeEach(async () => { - if (delegations) { - await Delegation.destroy({ - where: { - id: { [Op.in]: delegations.map((delegation) => delegation.id) }, - }, + describe.each([false, true])( + 'national registry v3 featureflag: %s', + (featureFlag) => { + describe.each(Object.keys(accessIncomingTestCases))( + 'Incoming Access with test case: %s', + (caseName) => { + const testCase = accessIncomingTestCases[caseName] + let app: TestApp + let server: request.SuperTest + let factory: FixtureFactory + let delegations: Delegation[] = [] + let delegationIndexService: DelegationsIndexService + + beforeAll(async () => { + // Arrange + app = await setupWithAuth({ + user: testCase.user, + customScopeRules: testCase.customScopeRules, + }) + server = request(app.getHttpServer()) + delegationIndexService = app.get(DelegationsIndexService) + const rskRelationshipsClientService = app.get( + RskRelationshipsClient, + ) + const nationalRegistryClientService = app.get( + NationalRegistryClientService, + ) + const nationalRegistryV3ClientService = app.get( + NationalRegistryV3ClientService, + ) + const companyRegistryClientService = app.get( + CompanyRegistryClientService, + ) + const nationalRegistryV3FeatureService = app.get( + NationalRegistryV3FeatureService, + ) + jest + .spyOn(nationalRegistryClientService, 'getCustodyChildren') + .mockImplementation(async () => []) + jest + .spyOn(nationalRegistryClientService, 'getIndividual') + .mockImplementation(async (nationalId: string) => + createNationalRegistryUser({ + nationalId, + name: fromName, + }), + ) + jest + .spyOn(nationalRegistryV3ClientService, 'getAllDataIndividual') + .mockImplementation(async (nationalId: string) => { + return { kennitala: nationalId, nafn: fromName } + }) + jest + .spyOn( + rskRelationshipsClientService, + 'getIndividualRelationships', + ) + .mockImplementation(async () => null) + jest + .spyOn(companyRegistryClientService, 'getCompany') + .mockImplementation(async () => null) + jest + .spyOn(delegationIndexService, 'indexDelegations') + .mockImplementation() + jest + .spyOn(delegationIndexService, 'indexCustomDelegations') + .mockImplementation() + jest + .spyOn(nationalRegistryV3FeatureService, 'getValue') + .mockImplementation(async () => featureFlag) + factory = new FixtureFactory(app) + await Promise.all( + testCase.domains.map((domain) => factory.createDomain(domain)), + ) + await Promise.all( + (testCase.accessTo ?? []).map((scope) => + factory.createApiScopeUserAccess({ + nationalId: testCase.user.nationalId, + scope, + }), + ), + ) + }) + + beforeEach(async () => { + if (delegations) { + await Delegation.destroy({ + where: { + id: { + [Op.in]: delegations.map((delegation) => delegation.id), + }, + }, + }) + } + + delegations = await Promise.all( + (testCase.delegations ?? []).map((delegation) => + factory.createCustomDelegation({ + toNationalId: testCase.user.nationalId, + fromName, + ...delegation, + }), + ), + ) }) - } - - delegations = await Promise.all( - (testCase.delegations ?? []).map((delegation) => - factory.createCustomDelegation({ - toNationalId: testCase.user.nationalId, - fromName, - ...delegation, - }), - ), - ) - }) - - it('GET /v1/me/delegations?direction=incoming filters delegations', async () => { - // Arrange - const expectedDelegations = filterExpectedDelegations( - delegations, - testCase.expected, - ) - - // Act - const res = await server.get('/v1/me/delegations?direction=incoming') - - // Assert - expect(delegationIndexService.indexDelegations).toHaveBeenCalled() - expect(res.status).toEqual(200) - expectMatchingDelegations(res.body, expectedDelegations) - }) - - if (testCase.expected.length > 0) { - it.each(testCase.expected)( - 'GET /v1/me/delegation/:id finds delegation in domain $name', - async (domain) => { + + it('GET /v1/me/delegations?direction=incoming filters delegations', async () => { // Arrange - const expectedDelegation = filterExpectedDelegations( + const expectedDelegations = filterExpectedDelegations( delegations, testCase.expected, - ).find((delegation) => delegation.domainName === domain.name) - assert(expectedDelegation) + ) // Act const res = await server.get( - `/v1/me/delegations/${expectedDelegation.id}`, + '/v1/me/delegations?direction=incoming', ) // Assert + expect(delegationIndexService.indexDelegations).toHaveBeenCalled() expect(res.status).toEqual(200) - expectMatchingDelegations(res.body, expectedDelegation) - }, - ) - - it.each(testCase.expected)( - 'DELETE /v1/me/delegations/:id removes access to delegation', - async (domain) => { - // Arrange - const delegationScopeModel = app.get( - getModelToken(DelegationScope), - ) - const delegationModel = app.get( - getModelToken(Delegation), - ) - const delegation = delegations.find( - (delegation) => - delegation.domainName === domain.name && - delegation.toNationalId === testCase.user.nationalId, - ) - assert(delegation) + expectMatchingDelegations(res.body, expectedDelegations) + }) - // Act - const res = await server.delete( - `/v1/me/delegations/${delegation.id}`, + if (testCase.expected.length > 0) { + it.each(testCase.expected)( + 'GET /v1/me/delegation/:id finds delegation in domain $name', + async (domain) => { + // Arrange + const expectedDelegation = filterExpectedDelegations( + delegations, + testCase.expected, + ).find((delegation) => delegation.domainName === domain.name) + assert(expectedDelegation) + + // Act + const res = await server.get( + `/v1/me/delegations/${expectedDelegation.id}`, + ) + + // Assert + expect(res.status).toEqual(200) + expectMatchingDelegations(res.body, expectedDelegation) + }, ) - // Assert - expect(res.status).toEqual(204) - expect( - delegationIndexService.indexCustomDelegations, - ).toHaveBeenCalled() - - const delegationAfter = await delegationModel.findByPk( - delegation.id, + it.each(testCase.expected)( + 'DELETE /v1/me/delegations/:id removes access to delegation', + async (domain) => { + // Arrange + const delegationScopeModel = app.get( + getModelToken(DelegationScope), + ) + const delegationModel = app.get( + getModelToken(Delegation), + ) + const delegation = delegations.find( + (delegation) => + delegation.domainName === domain.name && + delegation.toNationalId === testCase.user.nationalId, + ) + assert(delegation) + + // Act + const res = await server.delete( + `/v1/me/delegations/${delegation.id}`, + ) + + // Assert + expect(res.status).toEqual(204) + expect( + delegationIndexService.indexCustomDelegations, + ).toHaveBeenCalled() + + const delegationAfter = await delegationModel.findByPk( + delegation.id, + ) + expect(delegationAfter).toBeNull() + + const scopesAfter = await delegationScopeModel.findAll({ + where: { + delegationId: delegation.id, + scopeName: + delegation.delegationScopes?.map( + (scope) => scope.scopeName, + ) ?? [], + }, + }) + expect(scopesAfter).toHaveLength(0) + }, ) - expect(delegationAfter).toBeNull() - - const scopesAfter = await delegationScopeModel.findAll({ - where: { - delegationId: delegation.id, - scopeName: - delegation.delegationScopes?.map( - (scope) => scope.scopeName, - ) ?? [], + } + + if (testCase.expected.length === 0 && delegations.length > 0) { + it.each(delegations)( + 'GET /v1/me/delegation/:id returns no content response for $name', + async (delegation) => { + // Act + const res = await server.get( + `/v1/me/delegations/${delegation.id}`, + ) + + // Assert + expect(res.status).toEqual(204) + expect(res.body).toMatchObject({}) }, - }) - expect(scopesAfter).toHaveLength(0) - }, - ) - } - - if (testCase.expected.length === 0 && delegations.length > 0) { - it.each(delegations)( - 'GET /v1/me/delegation/:id returns no content response for $name', - async (delegation) => { - // Act - const res = await server.get(`/v1/me/delegations/${delegation.id}`) - - // Assert - expect(res.status).toEqual(204) - expect(res.body).toMatchObject({}) - }, - ) - } + ) + } + }, + ) }, ) }) diff --git a/apps/services/auth/ids-api/src/app/delegations/delegations-personal-representative.controller.spec.ts b/apps/services/auth/ids-api/src/app/delegations/delegations-personal-representative.controller.spec.ts index f0050b0d9e9e..5c07ba95334d 100644 --- a/apps/services/auth/ids-api/src/app/delegations/delegations-personal-representative.controller.spec.ts +++ b/apps/services/auth/ids-api/src/app/delegations/delegations-personal-representative.controller.spec.ts @@ -13,6 +13,7 @@ import { Domain, InactiveReason, MergedDelegationDTO, + NationalRegistryV3FeatureService, PersonalRepresentative, PersonalRepresentativeDelegationTypeModel, PersonalRepresentativeRight, @@ -22,6 +23,7 @@ import { UNKNOWN_NAME, } from '@island.is/auth-api-lib' import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientService } from '@island.is/clients/national-registry-v3' import { createClient, createDomain, @@ -54,374 +56,190 @@ import { } from '../../../test/stubs/personalRepresentativeStubs' describe('Personal Representative DelegationsController', () => { - describe('Given a user is authenticated', () => { - let app: TestApp - let factory: FixtureFactory - let server: request.SuperTest - let apiScopeModel: typeof ApiScope - let clientModel: typeof Client - let prScopePermission: typeof PersonalRepresentativeScopePermission - let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType - let prModel: typeof PersonalRepresentative - let prRightsModel: typeof PersonalRepresentativeRight - let prRightTypeModel: typeof PersonalRepresentativeRightType - let prTypeModel: typeof PersonalRepresentativeType - let prDelegationTypeModel: typeof PersonalRepresentativeDelegationTypeModel - let delegationTypeModel: typeof DelegationTypeModel - let nationalRegistryApi: NationalRegistryClientService - let delegationProviderModel: typeof DelegationProviderModel - let delegationIndexService: DelegationsIndexService - - const client = createClient({ - clientId: '@island.is/webapp', - }) - - const scopeValid1 = 'scope/valid1' - const scopeValid2 = 'scope/valid2' - const scopeValid1and2 = 'scope/valid1and2' - const scopeUnactiveType = 'scope/unactiveType' - const scopeOutdated = 'scope/outdated' - const disabledScope = 'disabledScope' - - client.allowedScopes = Object.values([ - scopeValid1, - scopeValid2, - scopeValid1and2, - scopeUnactiveType, - scopeOutdated, - disabledScope, - ]).map((s) => ({ - clientId: client.clientId, - scopeName: s, - })) - - const userNationalId = getFakeNationalId() - - const user = createCurrentUser({ - nationalId: userNationalId, - scope: [defaultScopes.testUserHasAccess.name], - client: client.clientId, - }) - - const domain = createDomain() - - beforeAll(async () => { - app = await setupWithAuth({ - user, - }) - server = request(app.getHttpServer()) - - prTypeModel = app.get( - getModelToken(PersonalRepresentativeType), - ) - - await prTypeModel.create(personalRepresentativeType) - - const domainModel = app.get(getModelToken(Domain)) - await domainModel.create(domain) - - apiScopeModel = app.get(getModelToken(ApiScope)) - prModel = app.get( - getModelToken(PersonalRepresentative), - ) - prRightsModel = app.get( - getModelToken(PersonalRepresentativeRight), - ) - prRightTypeModel = app.get( - getModelToken(PersonalRepresentativeRightType), - ) - prScopePermission = app.get( - getModelToken(PersonalRepresentativeScopePermission), - ) - apiScopeDelegationTypeModel = app.get( - getModelToken(ApiScopeDelegationType), - ) - prDelegationTypeModel = app.get< - typeof PersonalRepresentativeDelegationTypeModel - >(getModelToken(PersonalRepresentativeDelegationTypeModel)) - delegationTypeModel = app.get( - getModelToken(DelegationTypeModel), - ) - delegationProviderModel = app.get( - getModelToken(DelegationProviderModel), - ) - clientModel = app.get(getModelToken(Client)) - nationalRegistryApi = app.get(NationalRegistryClientService) - delegationIndexService = app.get(DelegationsIndexService) - factory = new FixtureFactory(app) - }) - - const createDelegationTypeAndProvider = async (rightCode: string[]) => { - const newDelegationProvider = await delegationProviderModel.create({ - id: AuthDelegationProvider.PersonalRepresentativeRegistry, - name: 'Talsmannagrunnur', - description: 'Talsmannagrunnur', - delegationTypes: [], - }) - - await delegationTypeModel.bulkCreate( - rightCode.map((code) => { - return { - id: getPersonalRepresentativeDelegationType(code), - providerId: newDelegationProvider.id, - name: getPersonalRepresentativeDelegationType(code), - description: `Personal representative delegation type for right type ${code}`, - } - }), - ) - } - - afterAll(async () => { - await app.cleanUp() - }) - - describe('and given we have 3 valid, 1 not yet active and 1 outdate right types', () => { - type rightsTypeStatus = 'valid' | 'unactivated' | 'outdated' - type rightsType = [code: string, status: rightsTypeStatus] - const rightsTypes: rightsType[] = [ - ['valid1', 'valid'], - ['valid2', 'valid'], - ['unactivated', 'unactivated'], - ['outdated', 'outdated'], - ] - - beforeAll(async () => { - await prRightTypeModel.bulkCreate( - rightsTypes.map(([code, status]) => { - switch (status) { - case 'valid': - return getPersonalRepresentativeRightType(code) - case 'unactivated': - return getPersonalRepresentativeRightType( - code, - faker.date.soon(7), - ) - case 'outdated': - return getPersonalRepresentativeRightType( - code, - faker.date.recent(5), - faker.date.recent(), - ) - } - }), - ) - await createDelegationTypeAndProvider(rightsTypes.map(([code]) => code)) - - client.supportedDelegationTypes = delegationTypes - await factory.createClient(client) - }) - - afterAll(async () => { - await prRightTypeModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, + describe.each([false, true])( + 'national registry v3 featureflag: %s', + (featureFlag) => { + describe('Given a user is authenticated', () => { + let app: TestApp + let factory: FixtureFactory + let server: request.SuperTest + let apiScopeModel: typeof ApiScope + let clientModel: typeof Client + let prScopePermission: typeof PersonalRepresentativeScopePermission + let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType + let prModel: typeof PersonalRepresentative + let prRightsModel: typeof PersonalRepresentativeRight + let prRightTypeModel: typeof PersonalRepresentativeRightType + let prTypeModel: typeof PersonalRepresentativeType + let prDelegationTypeModel: typeof PersonalRepresentativeDelegationTypeModel + let delegationTypeModel: typeof DelegationTypeModel + let nationalRegistryApi: NationalRegistryClientService + let nationalRegistryV3Api: NationalRegistryV3ClientService + let delegationProviderModel: typeof DelegationProviderModel + let delegationIndexService: DelegationsIndexService + + const client = createClient({ + clientId: '@island.is/webapp', }) - await delegationTypeModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - await delegationProviderModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, + + const scopeValid1 = 'scope/valid1' + const scopeValid2 = 'scope/valid2' + const scopeValid1and2 = 'scope/valid1and2' + const scopeUnactiveType = 'scope/unactiveType' + const scopeOutdated = 'scope/outdated' + const disabledScope = 'disabledScope' + + client.allowedScopes = Object.values([ + scopeValid1, + scopeValid2, + scopeValid1and2, + scopeUnactiveType, + scopeOutdated, + disabledScope, + ]).map((s) => ({ + clientId: client.clientId, + scopeName: s, + })) + + const userNationalId = getFakeNationalId() + + const user = createCurrentUser({ + nationalId: userNationalId, + scope: [defaultScopes.testUserHasAccess.name], + client: client.clientId, }) - }) - describe.each([ - [1, 0, 0, 2, 1], - [2, 0, 0, 1, 0], - [0, 0, 0, 0, 0], - [0, 1, 0, 0, 1], - [0, 0, 1, 0, 0], - [0, 1, 1, 0, 2], - [1, 1, 1, 0, 0], - ])( - 'and given user has %d active representees with valid rights, %d active representees with outdated rights and %d active representees with unactivated', - ( - valid: number, - outdated: number, - unactivated: number, - deceased: number, - nationalRegistryErrors: number, - ) => { - let nationalRegistryApiSpy: jest.SpyInstance - const validRepresentedPersons: NameIdTuple[] = [] - const outdatedRepresentedPersons: NameIdTuple[] = [] - const unactivatedRepresentedPersons: NameIdTuple[] = [] - const errorNationalIdsRepresentedPersons: NameIdTuple[] = [] - const deceasedNationalIds = times(deceased, getFakeNationalId) - const errorNationalIds = times( - nationalRegistryErrors, - getFakeNationalId, + const domain = createDomain() + + beforeAll(async () => { + app = await setupWithAuth({ + user, + }) + server = request(app.getHttpServer()) + + prTypeModel = app.get( + getModelToken(PersonalRepresentativeType), ) - beforeAll(async () => { - for (let i = 0; i < valid; i++) { - const representedPerson: NameIdTuple = [ - getFakeName(), - getFakeNationalId(), - ] - const relationship = getPersonalRepresentativeRelationship( - userNationalId, - representedPerson[1], - ) - validRepresentedPersons.push(representedPerson) - await prModel.create(relationship) - await prRightsModel.create( - getPersonalRepresentativeRights('valid1', relationship.id), - ) - await prDelegationTypeModel.create({ - personalRepresentativeId: relationship.id, - delegationTypeId: - getPersonalRepresentativeDelegationType('valid1'), - }) - } - - for (let i = 0; i < outdated; i++) { - const representedPerson: NameIdTuple = [ - getFakeName(), - getFakeNationalId(), - ] - const relationship = getPersonalRepresentativeRelationship( - userNationalId, - representedPerson[1], - ) - outdatedRepresentedPersons.push(representedPerson) - await prModel.create(relationship) - await prRightsModel.create( - getPersonalRepresentativeRights('outdated', relationship.id), - ) - await prDelegationTypeModel.create({ - personalRepresentativeId: relationship.id, - delegationTypeId: - getPersonalRepresentativeDelegationType('outdated'), - }) - } - - for (let i = 0; i < unactivated; i++) { - const representedPerson: NameIdTuple = [ - getFakeName(), - getFakeNationalId(), - ] - const relationship = getPersonalRepresentativeRelationship( - userNationalId, - representedPerson[1], - ) - unactivatedRepresentedPersons.push(representedPerson) - await prModel.create(relationship) - await prRightsModel.create( - getPersonalRepresentativeRights('unactivated', relationship.id), - ) - await prDelegationTypeModel.create({ - personalRepresentativeId: relationship.id, - delegationTypeId: - getPersonalRepresentativeDelegationType('unactivated'), - }) - } - - for (let i = 0; i < deceased; i++) { - const representedPerson: NameIdTuple = [ - getFakeName(), - deceasedNationalIds[i], - ] - const relationship = getPersonalRepresentativeRelationship( - userNationalId, - representedPerson[1], - ) + await prTypeModel.create(personalRepresentativeType) - await prModel.create(relationship) - await prRightsModel.create( - getPersonalRepresentativeRights('valid1', relationship.id), - ) - await prDelegationTypeModel.create({ - personalRepresentativeId: relationship.id, - delegationTypeId: - getPersonalRepresentativeDelegationType('valid1'), - }) - } - - for (let i = 0; i < nationalRegistryErrors; i++) { - const representedPerson: NameIdTuple = [ - UNKNOWN_NAME, - errorNationalIds[i], - ] - const relationship = getPersonalRepresentativeRelationship( - userNationalId, - representedPerson[1], - ) + const domainModel = app.get(getModelToken(Domain)) + await domainModel.create(domain) - errorNationalIdsRepresentedPersons.push(representedPerson) - // Create Personal Representative model which will have nationalIdRepresentedPerson throw an error - // when national registry api getIndividual is called - await prModel.create(relationship) - await prRightsModel.create( - getPersonalRepresentativeRights('valid1', relationship.id), - ) - await prDelegationTypeModel.create({ - personalRepresentativeId: relationship.id, - delegationTypeId: - getPersonalRepresentativeDelegationType('valid1'), - }) - } - - const nationalRegistryUsers = [ - ...validRepresentedPersons.map(([name, nationalId]) => - createNationalRegistryUser({ name, nationalId }), - ), - ...outdatedRepresentedPersons.map(([name, nationalId]) => - createNationalRegistryUser({ name, nationalId }), - ), - ...unactivatedRepresentedPersons.map(([name, nationalId]) => - createNationalRegistryUser({ name, nationalId }), - ), - ] + apiScopeModel = app.get(getModelToken(ApiScope)) + prModel = app.get( + getModelToken(PersonalRepresentative), + ) + prRightsModel = app.get( + getModelToken(PersonalRepresentativeRight), + ) + prRightTypeModel = app.get( + getModelToken(PersonalRepresentativeRightType), + ) + prScopePermission = app.get< + typeof PersonalRepresentativeScopePermission + >(getModelToken(PersonalRepresentativeScopePermission)) + apiScopeDelegationTypeModel = app.get( + getModelToken(ApiScopeDelegationType), + ) + prDelegationTypeModel = app.get< + typeof PersonalRepresentativeDelegationTypeModel + >(getModelToken(PersonalRepresentativeDelegationTypeModel)) + delegationTypeModel = app.get( + getModelToken(DelegationTypeModel), + ) + delegationProviderModel = app.get( + getModelToken(DelegationProviderModel), + ) + clientModel = app.get(getModelToken(Client)) + nationalRegistryApi = app.get(NationalRegistryClientService) + nationalRegistryV3Api = app.get(NationalRegistryV3ClientService) + delegationIndexService = app.get(DelegationsIndexService) + const nationalRegistryV3FeatureService = app.get( + NationalRegistryV3FeatureService, + ) + jest + .spyOn(nationalRegistryV3FeatureService, 'getValue') + .mockImplementation(async () => featureFlag) + factory = new FixtureFactory(app) + }) - nationalRegistryApiSpy = jest - .spyOn(nationalRegistryApi, 'getIndividual') - .mockImplementation(async (id) => { - if (deceasedNationalIds.includes(id)) { - return null - } + const createDelegationTypeAndProvider = async (rightCode: string[]) => { + const newDelegationProvider = await delegationProviderModel.create({ + id: AuthDelegationProvider.PersonalRepresentativeRegistry, + name: 'Talsmannagrunnur', + description: 'Talsmannagrunnur', + delegationTypes: [], + }) - if ( - errorNationalIds.find( - (errorNationalId) => id === errorNationalId, - ) - ) { - throw new Error('National registry error') - } + await delegationTypeModel.bulkCreate( + rightCode.map((code) => { + return { + id: getPersonalRepresentativeDelegationType(code), + providerId: newDelegationProvider.id, + name: getPersonalRepresentativeDelegationType(code), + description: `Personal representative delegation type for right type ${code}`, + } + }), + ) + } - const user = nationalRegistryUsers.find( - (u) => - u?.nationalId === id && - // Make sure we don't return a user that has been marked as deceased - !deceasedNationalIds.includes(u?.nationalId), - ) + afterAll(async () => { + await app.cleanUp() + }) - return user ?? null - }) + describe('and given we have 3 valid, 1 not yet active and 1 outdate right types', () => { + type rightsTypeStatus = 'valid' | 'unactivated' | 'outdated' + type rightsType = [code: string, status: rightsTypeStatus] + const rightsTypes: rightsType[] = [ + ['valid1', 'valid'], + ['valid2', 'valid'], + ['unactivated', 'unactivated'], + ['outdated', 'outdated'], + ] + + beforeAll(async () => { + await prRightTypeModel.bulkCreate( + rightsTypes.map(([code, status]) => { + switch (status) { + case 'valid': + return getPersonalRepresentativeRightType(code) + case 'unactivated': + return getPersonalRepresentativeRightType( + code, + faker.date.soon(7), + ) + case 'outdated': + return getPersonalRepresentativeRightType( + code, + faker.date.recent(5), + faker.date.recent(), + ) + } + }), + ) + await createDelegationTypeAndProvider( + rightsTypes.map(([code]) => code), + ) + + client.supportedDelegationTypes = delegationTypes + await factory.createClient(client) }) afterAll(async () => { - jest.clearAllMocks() - await prRightsModel.destroy({ + await prRightTypeModel.destroy({ where: {}, cascade: true, truncate: true, force: true, }) - await prModel.destroy({ + await delegationTypeModel.destroy({ where: {}, cascade: true, truncate: true, force: true, }) - await prDelegationTypeModel.destroy({ + await delegationProviderModel.destroy({ where: {}, cascade: true, truncate: true, @@ -429,215 +247,409 @@ describe('Personal Representative DelegationsController', () => { }) }) - describe('when user calls GET /v2/delegations', () => { - const path = '/v2/delegations' - let response: request.Response - let body: MergedDelegationDTO[] + describe.each([ + [1, 0, 0, 2, 1], + [2, 0, 0, 1, 0], + [0, 0, 0, 0, 0], + [0, 1, 0, 0, 1], + [0, 0, 1, 0, 0], + [0, 1, 1, 0, 2], + [1, 1, 1, 0, 0], + ])( + 'and given user has %d active representees with valid rights, %d active representees with outdated rights and %d active representees with unactivated', + ( + valid: number, + outdated: number, + unactivated: number, + deceased: number, + nationalRegistryErrors: number, + ) => { + let nationalRegistryApiSpy: jest.SpyInstance + let nationalRegistryV3ApiSpy: jest.SpyInstance + const validRepresentedPersons: NameIdTuple[] = [] + const outdatedRepresentedPersons: NameIdTuple[] = [] + const unactivatedRepresentedPersons: NameIdTuple[] = [] + const errorNationalIdsRepresentedPersons: NameIdTuple[] = [] + const deceasedNationalIds = times(deceased, getFakeNationalId) + const errorNationalIds = times( + nationalRegistryErrors, + getFakeNationalId, + ) - beforeAll(async () => { - response = await server.get(path) - body = response.body - }) + beforeAll(async () => { + for (let i = 0; i < valid; i++) { + const representedPerson: NameIdTuple = [ + getFakeName(), + getFakeNationalId(), + ] + const relationship = getPersonalRepresentativeRelationship( + userNationalId, + representedPerson[1], + ) + validRepresentedPersons.push(representedPerson) + await prModel.create(relationship) + await prRightsModel.create( + getPersonalRepresentativeRights('valid1', relationship.id), + ) + await prDelegationTypeModel.create({ + personalRepresentativeId: relationship.id, + delegationTypeId: + getPersonalRepresentativeDelegationType('valid1'), + }) + } - it('should have a an OK return status', () => { - expect(response.status).toEqual(200) - }) + for (let i = 0; i < outdated; i++) { + const representedPerson: NameIdTuple = [ + getFakeName(), + getFakeNationalId(), + ] + const relationship = getPersonalRepresentativeRelationship( + userNationalId, + representedPerson[1], + ) + outdatedRepresentedPersons.push(representedPerson) + await prModel.create(relationship) + await prRightsModel.create( + getPersonalRepresentativeRights( + 'outdated', + relationship.id, + ), + ) + await prDelegationTypeModel.create({ + personalRepresentativeId: relationship.id, + delegationTypeId: + getPersonalRepresentativeDelegationType('outdated'), + }) + } - it(`should return ${valid} ${ - valid === 1 ? 'item' : 'items' - } `, () => { - expect(body).toHaveLength(valid + nationalRegistryErrors) - }) + for (let i = 0; i < unactivated; i++) { + const representedPerson: NameIdTuple = [ + getFakeName(), + getFakeNationalId(), + ] + const relationship = getPersonalRepresentativeRelationship( + userNationalId, + representedPerson[1], + ) + unactivatedRepresentedPersons.push(representedPerson) + await prModel.create(relationship) + await prRightsModel.create( + getPersonalRepresentativeRights( + 'unactivated', + relationship.id, + ), + ) + await prDelegationTypeModel.create({ + personalRepresentativeId: relationship.id, + delegationTypeId: + getPersonalRepresentativeDelegationType('unactivated'), + }) + } - it('should have the nationalId of the user as the representer', () => { - expect( - body.every((d) => d.toNationalId === userNationalId), - ).toBeTruthy() - }) + for (let i = 0; i < deceased; i++) { + const representedPerson: NameIdTuple = [ + getFakeName(), + deceasedNationalIds[i], + ] + const relationship = getPersonalRepresentativeRelationship( + userNationalId, + representedPerson[1], + ) - it('should only have the nationalId of the valid representees', () => { - expect(body.map((d) => d.fromNationalId).sort()).toEqual( - [ - ...validRepresentedPersons.map(([_, id]) => id), - ...errorNationalIdsRepresentedPersons.map(([_, id]) => id), - ].sort(), - ) - }) + await prModel.create(relationship) + await prRightsModel.create( + getPersonalRepresentativeRights('valid1', relationship.id), + ) + await prDelegationTypeModel.create({ + personalRepresentativeId: relationship.id, + delegationTypeId: + getPersonalRepresentativeDelegationType('valid1'), + }) + } - it(`should only have ${ - valid + nationalRegistryErrors === 1 ? 'name' : 'names' - } of the valid represented ${ - valid + nationalRegistryErrors === 1 ? 'person' : 'persons' - }`, () => { - expect(body.map((d) => d.fromName).sort()).toEqual([ - ...validRepresentedPersons.map(([name, _]) => name).sort(), - ...errorNationalIdsRepresentedPersons.map(([name]) => name), - ]) - }) + for (let i = 0; i < nationalRegistryErrors; i++) { + const representedPerson: NameIdTuple = [ + UNKNOWN_NAME, + errorNationalIds[i], + ] + const relationship = getPersonalRepresentativeRelationship( + userNationalId, + representedPerson[1], + ) - it(`should have fetched the ${ - valid + deceased + nationalRegistryErrors === 1 ? 'name' : 'names' - } of the valid represented ${ - valid + deceased + nationalRegistryErrors === 1 - ? 'person' - : 'persons' - } from nationalRegistryApi`, () => { - expect(nationalRegistryApiSpy).toHaveBeenCalledTimes( - valid + deceased + nationalRegistryErrors, - ) - }) + errorNationalIdsRepresentedPersons.push(representedPerson) + // Create Personal Representative model which will have nationalIdRepresentedPerson throw an error + // when national registry api getIndividual is called + await prModel.create(relationship) + await prRightsModel.create( + getPersonalRepresentativeRights('valid1', relationship.id), + ) + await prDelegationTypeModel.create({ + personalRepresentativeId: relationship.id, + delegationTypeId: + getPersonalRepresentativeDelegationType('valid1'), + }) + } - it('should have the delegation type claims of PersonalRepresentative', () => { - expect( - body.every( - (d) => - d.types[0] === AuthDelegationType.PersonalRepresentative, - ), - ).toBeTruthy() - }) + const nationalRegistryUsers = [ + ...validRepresentedPersons.map(([name, nationalId]) => + createNationalRegistryUser({ name, nationalId }), + ), + ...outdatedRepresentedPersons.map(([name, nationalId]) => + createNationalRegistryUser({ name, nationalId }), + ), + ...unactivatedRepresentedPersons.map(([name, nationalId]) => + createNationalRegistryUser({ name, nationalId }), + ), + ] + + nationalRegistryApiSpy = jest + .spyOn(nationalRegistryApi, 'getIndividual') + .mockImplementation(async (id) => { + if (deceasedNationalIds.includes(id)) { + return null + } + + if ( + errorNationalIds.find( + (errorNationalId) => id === errorNationalId, + ) + ) { + throw new Error('National registry error') + } + + const user = nationalRegistryUsers.find( + (u) => + u?.nationalId === id && + // Make sure we don't return a user that has been marked as deceased + !deceasedNationalIds.includes(u?.nationalId), + ) + + return user ?? null + }) + + nationalRegistryV3ApiSpy = jest + .spyOn(nationalRegistryV3Api, 'getAllDataIndividual') + .mockImplementation(async (id) => { + if ( + errorNationalIds.find( + (errorNationalId) => id === errorNationalId, + ) + ) { + throw new Error('National registry error') + } + + if (deceasedNationalIds.includes(id)) { + return { + kennitala: id, + afdrif: 'LÉST', + } + } + + const user = nationalRegistryUsers.find( + (u) => u?.nationalId === id, + ) + + return user + ? { + kennitala: id, + nafn: user?.name, + afdrif: null, + } + : null + }) + }) - it('should have made prModels inactive for deceased persons', async () => { - // Arrange - const expectedModels = await prModel.findAll({ - where: { - nationalIdRepresentedPerson: deceasedNationalIds, - inactive: true, - inactiveReason: InactiveReason.DECEASED_PARTY, - }, + afterAll(async () => { + jest.clearAllMocks() + await prRightsModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await prModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await prDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) }) - // Assert - expect(expectedModels.length).toEqual(deceased) + describe('when user calls GET /v2/delegations', () => { + const path = '/v2/delegations' + let response: request.Response + let body: MergedDelegationDTO[] - expectedModels.forEach((model) => { - expect(model.inactive).toEqual(true) - expect(model.inactiveReason).toEqual( - InactiveReason.DECEASED_PARTY, - ) - }) - }) + beforeAll(async () => { + response = await server.get(path) + body = response.body + }) - it('should return delegation if national registry api getIndividual throws an error', async () => { - // Arrange - const expectedModels = await prModel.findAll({ - where: { - nationalIdRepresentedPerson: errorNationalIds, - }, - }) + it('should have a an OK return status', () => { + expect(response.status).toEqual(200) + }) - // Assert - expect(expectedModels.length).toEqual(errorNationalIds.length) - }) + it(`should return ${valid} ${ + valid === 1 ? 'item' : 'items' + } `, () => { + expect(body).toHaveLength(valid + nationalRegistryErrors) + }) - it('should index delegations', () => { - expect(delegationIndexService.indexDelegations).toHaveBeenCalled() - }) - }) - }, - ) - - describe('and given we have a combination of scopes for personal representative', () => { - type scopesType = [name: string, enabled: boolean, rightTypes: string[]] - const scopes: scopesType[] = [ - [scopeValid1, true, ['valid1']], - [scopeValid2, true, ['valid2']], - [scopeValid1and2, true, ['valid1', 'valid2']], - [scopeUnactiveType, true, ['unactivated']], - [scopeOutdated, true, ['outdated']], - [disabledScope, false, ['valid1']], - ] + it('should have the nationalId of the user as the representer', () => { + expect( + body.every((d) => d.toNationalId === userNationalId), + ).toBeTruthy() + }) - beforeAll(async () => { - const apiScopes = scopes.flatMap(([name, enabled, types]) => ({ - name: name, - enabled, - domainName: domain.name, - supportedDelegationTypes: types.map((rt) => - getPersonalRepresentativeDelegationType(rt), - ), - })) - - await Promise.all( - apiScopes.map((scope) => factory.createApiScope(scope)), - ) + it('should only have the nationalId of the valid representees', () => { + expect(body.map((d) => d.fromNationalId).sort()).toEqual( + [ + ...validRepresentedPersons.map(([_, id]) => id), + ...errorNationalIdsRepresentedPersons.map( + ([_, id]) => id, + ), + ].sort(), + ) + }) - await prScopePermission.bulkCreate( - scopes.flatMap(([name, _, types]) => - types.map((rt) => getScopePermission(rt, name)), - ), - ) - }) + it(`should only have ${ + valid + nationalRegistryErrors === 1 ? 'name' : 'names' + } of the valid represented ${ + valid + nationalRegistryErrors === 1 ? 'person' : 'persons' + }`, () => { + expect(body.map((d) => d.fromName).sort()).toEqual([ + ...validRepresentedPersons.map(([name, _]) => name).sort(), + ...errorNationalIdsRepresentedPersons.map(([name]) => name), + ]) + }) - afterAll(async () => { - await prScopePermission.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - await apiScopeDelegationTypeModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - await apiScopeModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - }) + it(`should have fetched the ${ + valid + deceased + nationalRegistryErrors === 1 + ? 'name' + : 'names' + } of the valid represented ${ + valid + deceased + nationalRegistryErrors === 1 + ? 'person' + : 'persons' + } from nationalRegistryApi`, () => { + featureFlag + ? expect(nationalRegistryV3ApiSpy).toHaveBeenCalledTimes( + valid + deceased + nationalRegistryErrors, + ) + : expect(nationalRegistryApiSpy).toHaveBeenCalledTimes( + valid + deceased + nationalRegistryErrors, + ) + }) + + it('should have the delegation type claims of PersonalRepresentative', () => { + expect( + body.every( + (d) => + d.types[0] === + AuthDelegationType.PersonalRepresentative, + ), + ).toBeTruthy() + }) + + it('should have made prModels inactive for deceased persons', async () => { + // Arrange + const expectedModels = await prModel.findAll({ + where: { + nationalIdRepresentedPerson: deceasedNationalIds, + inactive: true, + inactiveReason: InactiveReason.DECEASED_PARTY, + }, + }) + + // Assert + expect(expectedModels.length).toEqual(deceased) + + expectedModels.forEach((model) => { + expect(model.inactive).toEqual(true) + expect(model.inactiveReason).toEqual( + InactiveReason.DECEASED_PARTY, + ) + }) + }) - describe.each([ - [['valid1'], [scopeValid1, scopeValid1and2]], - [['valid2'], [scopeValid2, scopeValid1and2]], - [ - ['valid1', 'valid2'], - [scopeValid1, scopeValid2, scopeValid1and2], - ], - [[], []], - // [['unactivated'], []], - // [['outdated'], []], - ])( - 'and given user is representing persons with rights %p', - (rights, expected) => { - const representeeNationalId = getFakeNationalId() + it('should return delegation if national registry api getIndividual throws an error', async () => { + // Arrange + const expectedModels = await prModel.findAll({ + where: { + nationalIdRepresentedPerson: errorNationalIds, + }, + }) + + // Assert + expect(expectedModels.length).toEqual(errorNationalIds.length) + }) + + it('should index delegations', () => { + expect( + delegationIndexService.indexDelegations, + ).toHaveBeenCalled() + }) + }) + }, + ) + + describe('and given we have a combination of scopes for personal representative', () => { + type scopesType = [ + name: string, + enabled: boolean, + rightTypes: string[], + ] + const scopes: scopesType[] = [ + [scopeValid1, true, ['valid1']], + [scopeValid2, true, ['valid2']], + [scopeValid1and2, true, ['valid1', 'valid2']], + [scopeUnactiveType, true, ['unactivated']], + [scopeOutdated, true, ['outdated']], + [disabledScope, false, ['valid1']], + ] beforeAll(async () => { - const relationship = getPersonalRepresentativeRelationship( - userNationalId, - representeeNationalId, + const apiScopes = scopes.flatMap(([name, enabled, types]) => ({ + name: name, + enabled, + domainName: domain.name, + supportedDelegationTypes: types.map((rt) => + getPersonalRepresentativeDelegationType(rt), + ), + })) + + await Promise.all( + apiScopes.map((scope) => factory.createApiScope(scope)), ) - await prModel.create(relationship) - await prRightsModel.bulkCreate( - rights.map((r) => - getPersonalRepresentativeRights(r, relationship.id), + await prScopePermission.bulkCreate( + scopes.flatMap(([name, _, types]) => + types.map((rt) => getScopePermission(rt, name)), ), ) - await prDelegationTypeModel.bulkCreate( - rights.map((r) => ({ - personalRepresentativeId: relationship.id, - delegationTypeId: getPersonalRepresentativeDelegationType(r), - })), - ) }) afterAll(async () => { - await prRightsModel.destroy({ + await prScopePermission.destroy({ where: {}, cascade: true, truncate: true, force: true, }) - await prModel.destroy({ + await apiScopeDelegationTypeModel.destroy({ where: {}, cascade: true, truncate: true, force: true, }) - await prDelegationTypeModel.destroy({ + await apiScopeModel.destroy({ where: {}, cascade: true, truncate: true, @@ -645,34 +657,95 @@ describe('Personal Representative DelegationsController', () => { }) }) - describe('when user calls GET /delegations/scopes', () => { - const path = '/delegations/scopes' - let response: request.Response - let body: string[] + describe.each([ + [['valid1'], [scopeValid1, scopeValid1and2]], + [['valid2'], [scopeValid2, scopeValid1and2]], + [ + ['valid1', 'valid2'], + [scopeValid1, scopeValid2, scopeValid1and2], + ], + [[], []], + // [['unactivated'], []], + // [['outdated'], []], + ])( + 'and given user is representing persons with rights %p', + (rights, expected) => { + const representeeNationalId = getFakeNationalId() + + beforeAll(async () => { + const relationship = getPersonalRepresentativeRelationship( + userNationalId, + representeeNationalId, + ) - beforeAll(async () => { - response = await server.get(`${path}`).query({ - fromNationalId: representeeNationalId, - delegationType: rights.map((r) => - getPersonalRepresentativeDelegationType(r), - ), + await prModel.create(relationship) + await prRightsModel.bulkCreate( + rights.map((r) => + getPersonalRepresentativeRights(r, relationship.id), + ), + ) + await prDelegationTypeModel.bulkCreate( + rights.map((r) => ({ + personalRepresentativeId: relationship.id, + delegationTypeId: + getPersonalRepresentativeDelegationType(r), + })), + ) }) - body = response.body - }) - it('should have a an OK return status', () => { - expect(response.status).toEqual(200) - }) + afterAll(async () => { + await prRightsModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await prModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await prDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + }) - it(`should return ${ - expected.length === 0 ? 'no scopes' : JSON.stringify(expected) - }`, () => { - expect(body.sort()).toEqual(expected.sort()) - }) - }) - }, - ) + describe('when user calls GET /delegations/scopes', () => { + const path = '/delegations/scopes' + let response: request.Response + let body: string[] + + beforeAll(async () => { + response = await server.get(`${path}`).query({ + fromNationalId: representeeNationalId, + delegationType: rights.map((r) => + getPersonalRepresentativeDelegationType(r), + ), + }) + body = response.body + }) + + it('should have a an OK return status', () => { + expect(response.status).toEqual(200) + }) + + it(`should return ${ + expected.length === 0 + ? 'no scopes' + : JSON.stringify(expected) + }`, () => { + expect(body.sort()).toEqual(expected.sort()) + }) + }) + }, + ) + }) + }) }) - }) - }) + }, + ) }) diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts index 526bf4262a4a..eb6c5a885989 100644 --- a/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts +++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts @@ -4,9 +4,13 @@ import faker from 'faker' import { Sequelize } from 'sequelize-typescript' import request from 'supertest' -import { MergedDelegationDTO } from '@island.is/auth-api-lib' +import { + MergedDelegationDTO, + NationalRegistryV3FeatureService, +} from '@island.is/auth-api-lib' import { RskRelationshipsClient } from '@island.is/clients-rsk-relationships' import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientService } from '@island.is/clients/national-registry-v3' import { FixtureFactory } from '@island.is/services/auth/testing' import { AuthDelegationProvider, @@ -23,166 +27,190 @@ import { testCases } from './delegations-filters-test-cases' import { user } from './delegations-filters-types' describe('DelegationsController', () => { - let sequelize: Sequelize - let app: TestApp - let server: request.SuperTest - let factory: FixtureFactory - let nationalRegistryApi: NationalRegistryClientService - let rskApi: RskRelationshipsClient - beforeAll(async () => { - app = await setupWithAuth({ - user: user, - }) - sequelize = await app.resolve(getConnectionToken() as Type) - - server = request(app.getHttpServer()) - - nationalRegistryApi = app.get(NationalRegistryClientService) - jest - .spyOn(nationalRegistryApi, 'getIndividual') - .mockImplementation(async (nationalId: string) => - createNationalRegistryUser({ - nationalId, - name: faker.name.findName(), - }), - ) - rskApi = app.get(RskRelationshipsClient) - - factory = new FixtureFactory(app) - }) - - afterAll(async () => { - await app.cleanUp() - }) - - describe.each(Object.keys(testCases))( - 'Delegation filtering with test case: %s', - (caseName) => { - const testCase = testCases[caseName] - testCase.user = user - const path = '/v2/delegations' - + describe.each([false, true])( + 'national registry v3 featureflag: %s', + (featureFlag) => { + let sequelize: Sequelize + let app: TestApp + let server: request.SuperTest + let factory: FixtureFactory + let nationalRegistryApi: NationalRegistryClientService + let nationalRegistryV3Api: NationalRegistryV3ClientService + let rskApi: RskRelationshipsClient beforeAll(async () => { - await truncate(sequelize) - - await Promise.all( - testCase.domains.map((domain) => factory.createDomain(domain)), - ) + app = await setupWithAuth({ + user: user, + }) + sequelize = await app.resolve(getConnectionToken() as Type) - await factory.createClient(testCase.client) - - await Promise.all( - testCase.clientAllowedScopes.map((scope) => - factory.createClientAllowedScope(scope), - ), - ) + server = request(app.getHttpServer()) - await Promise.all( - testCase.apiScopes.map((scope) => factory.createApiScope(scope)), - ) - - await Promise.all( - testCase.apiScopeUserAccess.map((access) => - factory.createApiScopeUserAccess(access), - ), - ) - - await Promise.all( - testCase.customDelegations.map((delegation) => - factory.createCustomDelegation(delegation), - ), - ) - - await Promise.all( - testCase.fromLegalRepresentative.map((nationalId) => - factory.createDelegationIndexRecord({ - fromNationalId: nationalId, - toNationalId: testCase.user.nationalId, - type: AuthDelegationType.LegalRepresentative, - provider: AuthDelegationProvider.DistrictCommissionersRegistry, + nationalRegistryApi = app.get(NationalRegistryClientService) + jest + .spyOn(nationalRegistryApi, 'getIndividual') + .mockImplementation(async (nationalId: string) => + createNationalRegistryUser({ + nationalId, + name: faker.name.findName(), }), - ), + ) + nationalRegistryV3Api = app.get(NationalRegistryV3ClientService) + jest + .spyOn(nationalRegistryV3Api, 'getAllDataIndividual') + .mockImplementation(async (nationalId: string) => { + const user = createNationalRegistryUser({ + nationalId: nationalId, + }) + + return { kennitala: user.nationalId, nafn: user.name } + }) + rskApi = app.get(RskRelationshipsClient) + + const nationalRegistryV3FeatureService = app.get( + NationalRegistryV3FeatureService, ) - jest - .spyOn(nationalRegistryApi, 'getCustodyChildren') - .mockImplementation(async () => testCase.fromChildren) + .spyOn(nationalRegistryV3FeatureService, 'getValue') + .mockImplementation(async () => featureFlag) - jest - .spyOn(rskApi, 'getIndividualRelationships') - .mockImplementation(async () => testCase.procuration) + factory = new FixtureFactory(app) }) - let res: request.Response - - it(`GET ${path} returns correct filtered delegations`, async () => { - res = await server.get(path) - - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(testCase.expectedFrom.length) - expect( - res.body.map((d: MergedDelegationDTO) => d.fromNationalId).sort(), - ).toEqual(testCase.expectedFrom.sort()) - if (testCase.expectedTypes) { - expect(res.body[0].types.sort()).toEqual( - testCase.expectedTypes.sort(), - ) - } + afterAll(async () => { + await app.cleanUp() }) - }, - ) - - describe('verify', () => { - const testCase = testCases['legalRepresentative1'] - testCase.user = user - const path = '/v1/delegations/verify' - beforeAll(async () => { - await truncate(sequelize) - - await Promise.all( - testCase.domains.map((domain) => factory.createDomain(domain)), + describe.each(Object.keys(testCases))( + 'Delegation filtering with test case: %s', + (caseName) => { + const testCase = testCases[caseName] + testCase.user = user + const path = '/v2/delegations' + + beforeAll(async () => { + await truncate(sequelize) + + await Promise.all( + testCase.domains.map((domain) => factory.createDomain(domain)), + ) + + await factory.createClient(testCase.client) + + await Promise.all( + testCase.clientAllowedScopes.map((scope) => + factory.createClientAllowedScope(scope), + ), + ) + + await Promise.all( + testCase.apiScopes.map((scope) => factory.createApiScope(scope)), + ) + + await Promise.all( + testCase.apiScopeUserAccess.map((access) => + factory.createApiScopeUserAccess(access), + ), + ) + + await Promise.all( + testCase.customDelegations.map((delegation) => + factory.createCustomDelegation(delegation), + ), + ) + + await Promise.all( + testCase.fromLegalRepresentative.map((nationalId) => + factory.createDelegationIndexRecord({ + fromNationalId: nationalId, + toNationalId: testCase.user.nationalId, + type: AuthDelegationType.LegalRepresentative, + provider: + AuthDelegationProvider.DistrictCommissionersRegistry, + }), + ), + ) + + jest + .spyOn(nationalRegistryApi, 'getCustodyChildren') + .mockImplementation(async () => testCase.fromChildren) + + jest + .spyOn(rskApi, 'getIndividualRelationships') + .mockImplementation(async () => testCase.procuration) + }) + + let res: request.Response + + it(`GET ${path} returns correct filtered delegations`, async () => { + res = await server.get(path) + + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(testCase.expectedFrom.length) + expect( + res.body.map((d: MergedDelegationDTO) => d.fromNationalId).sort(), + ).toEqual(testCase.expectedFrom.sort()) + if (testCase.expectedTypes) { + expect(res.body[0].types.sort()).toEqual( + testCase.expectedTypes.sort(), + ) + } + }) + }, ) - await factory.createClient(testCase.client) + describe('verify', () => { + const testCase = testCases['legalRepresentative1'] + testCase.user = user + const path = '/v1/delegations/verify' - await Promise.all( - testCase.clientAllowedScopes.map((scope) => - factory.createClientAllowedScope(scope), - ), - ) + beforeAll(async () => { + await truncate(sequelize) - await Promise.all( - testCase.apiScopes.map((scope) => factory.createApiScope(scope)), - ) + await Promise.all( + testCase.domains.map((domain) => factory.createDomain(domain)), + ) - await factory.createDelegationIndexRecord({ - fromNationalId: nonExistingLegalRepresentativeNationalId, - toNationalId: testCase.user.nationalId, - type: AuthDelegationType.LegalRepresentative, - provider: AuthDelegationProvider.DistrictCommissionersRegistry, - }) - }) + await factory.createClient(testCase.client) - let res: request.Response - it(`POST ${path} returns verified response`, async () => { - res = await server.post(path).send({ - fromNationalId: testCase.fromLegalRepresentative[0], - delegationTypes: [AuthDelegationType.LegalRepresentative], - }) + await Promise.all( + testCase.clientAllowedScopes.map((scope) => + factory.createClientAllowedScope(scope), + ), + ) - expect(res.status).toEqual(200) - expect(res.body.verified).toEqual(true) - }) + await Promise.all( + testCase.apiScopes.map((scope) => factory.createApiScope(scope)), + ) - it(`POST ${path} returns non-verified response`, async () => { - res = await server.post(path).send({ - fromNationalId: nonExistingLegalRepresentativeNationalId, - delegationTypes: [AuthDelegationType.LegalRepresentative], + await factory.createDelegationIndexRecord({ + fromNationalId: nonExistingLegalRepresentativeNationalId, + toNationalId: testCase.user.nationalId, + type: AuthDelegationType.LegalRepresentative, + provider: AuthDelegationProvider.DistrictCommissionersRegistry, + }) + }) + + let res: request.Response + it(`POST ${path} returns verified response`, async () => { + res = await server.post(path).send({ + fromNationalId: testCase.fromLegalRepresentative[0], + delegationTypes: [AuthDelegationType.LegalRepresentative], + }) + + expect(res.status).toEqual(200) + expect(res.body.verified).toEqual(true) + }) + + it(`POST ${path} returns non-verified response`, async () => { + res = await server.post(path).send({ + fromNationalId: nonExistingLegalRepresentativeNationalId, + delegationTypes: [AuthDelegationType.LegalRepresentative], + }) + + expect(res.status).toEqual(200) + expect(res.body.verified).toEqual(false) + }) }) - - expect(res.status).toEqual(200) - expect(res.body.verified).toEqual(false) - }) - }) + }, + ) }) diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts index 4abd4987bd31..f2dee16513d6 100644 --- a/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts +++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts @@ -1,7 +1,7 @@ import { getModelToken } from '@nestjs/sequelize' +import addDays from 'date-fns/addDays' import request from 'supertest' import { uuid } from 'uuidv4' -import addDays from 'date-fns/addDays' import { ApiScope, @@ -12,8 +12,10 @@ import { DelegationScope, DelegationTypeModel, Domain, + NationalRegistryV3FeatureService, } from '@island.is/auth-api-lib' import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientService } from '@island.is/clients/national-registry-v3' import { createClient, createDomain, @@ -33,246 +35,273 @@ import { defaultScopes, setupWithAuth } from '../../../../test/setup' import { getFakeNationalId } from '../../../../test/stubs/genericStubs' describe('DelegationsController', () => { - describe('Given a user is authenticated', () => { - let app: TestApp - let factory: FixtureFactory - let server: request.SuperTest - - let apiScopeModel: typeof ApiScope - let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType - let delegationDelegationTypeModel: typeof DelegationDelegationType - let delegationModel: typeof Delegation - let delegationTypeModel: typeof DelegationTypeModel - let nationalRegistryApi: NationalRegistryClientService - let delegationProviderModel: typeof DelegationProviderModel - let delegationScopesModel: typeof DelegationScope - - const client = createClient({ - clientId: '@island.is/webapp', - }) - - const scopeValid1 = 'scope/valid1' - const scopeValid2 = 'scope/valid2' - const scopeValid1and2 = 'scope/valid1and2' - const scopeUnactiveType = 'scope/unactiveType' - const scopeOutdated = 'scope/outdated' - const disabledScope = 'disabledScope' - - client.allowedScopes = Object.values([ - scopeValid1, - scopeValid2, - scopeValid1and2, - scopeUnactiveType, - scopeOutdated, - disabledScope, - ]).map((s) => ({ - clientId: client.clientId, - scopeName: s, - })) - - const userNationalId = getFakeNationalId() - - const user = createCurrentUser({ - nationalId: userNationalId, - scope: [defaultScopes.testUserHasAccess.name], - client: client.clientId, - }) - - const domain = createDomain() - - beforeAll(async () => { - app = await setupWithAuth({ - user, - }) - server = request(app.getHttpServer()) - - const domainModel = app.get(getModelToken(Domain)) - await domainModel.create(domain) - - apiScopeModel = app.get(getModelToken(ApiScope)) - - apiScopeDelegationTypeModel = app.get( - getModelToken(ApiScopeDelegationType), - ) - delegationTypeModel = app.get( - getModelToken(DelegationTypeModel), - ) - delegationProviderModel = app.get( - getModelToken(DelegationProviderModel), - ) - delegationScopesModel = app.get( - getModelToken(DelegationScope), - ) - delegationModel = app.get(getModelToken(Delegation)) - delegationDelegationTypeModel = app.get( - getModelToken(DelegationDelegationType), - ) - nationalRegistryApi = app.get(NationalRegistryClientService) - factory = new FixtureFactory(app) - }) - - afterAll(async () => { - await app.cleanUp() - }) - - describe('GET with general mandate delegation type', () => { - const representeeNationalId = getFakeNationalId() - let nationalRegistryApiSpy: jest.SpyInstance - const scopeNames = [ - 'api-scope/generalMandate1', - 'api-scope/generalMandate2', - 'api-scope/generalMandate3', - ] - - beforeAll(async () => { - client.supportedDelegationTypes = [ - AuthDelegationType.GeneralMandate, - AuthDelegationType.LegalGuardian, - ] - await factory.createClient(client) - - const delegations = await delegationModel.create({ - id: uuid(), - fromDisplayName: 'Test', - fromNationalId: representeeNationalId, - toNationalId: userNationalId, - toName: 'Test', + describe.each([false, true])( + 'national registry v3 featureflag: %s', + (featureFlag) => { + describe('Given a user is authenticated', () => { + let app: TestApp + let factory: FixtureFactory + let server: request.SuperTest + + let apiScopeModel: typeof ApiScope + let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType + let delegationDelegationTypeModel: typeof DelegationDelegationType + let delegationModel: typeof Delegation + let delegationTypeModel: typeof DelegationTypeModel + let nationalRegistryApi: NationalRegistryClientService + let nationalRegistryV3Api: NationalRegistryV3ClientService + let delegationProviderModel: typeof DelegationProviderModel + let delegationScopesModel: typeof DelegationScope + + const client = createClient({ + clientId: '@island.is/webapp', + }) + + const scopeValid1 = 'scope/valid1' + const scopeValid2 = 'scope/valid2' + const scopeValid1and2 = 'scope/valid1and2' + const scopeUnactiveType = 'scope/unactiveType' + const scopeOutdated = 'scope/outdated' + const disabledScope = 'disabledScope' + + client.allowedScopes = Object.values([ + scopeValid1, + scopeValid2, + scopeValid1and2, + scopeUnactiveType, + scopeOutdated, + disabledScope, + ]).map((s) => ({ + clientId: client.clientId, + scopeName: s, + })) + + const userNationalId = getFakeNationalId() + + const user = createCurrentUser({ + nationalId: userNationalId, + scope: [defaultScopes.testUserHasAccess.name], + client: client.clientId, }) - await delegationProviderModel.create({ - id: AuthDelegationProvider.Custom, - name: 'Custom', - description: 'Custom', + const domain = createDomain() + + beforeAll(async () => { + app = await setupWithAuth({ + user, + }) + server = request(app.getHttpServer()) + + const domainModel = app.get(getModelToken(Domain)) + await domainModel.create(domain) + + apiScopeModel = app.get(getModelToken(ApiScope)) + + apiScopeDelegationTypeModel = app.get( + getModelToken(ApiScopeDelegationType), + ) + delegationTypeModel = app.get( + getModelToken(DelegationTypeModel), + ) + delegationProviderModel = app.get( + getModelToken(DelegationProviderModel), + ) + delegationScopesModel = app.get( + getModelToken(DelegationScope), + ) + delegationModel = app.get( + getModelToken(Delegation), + ) + delegationDelegationTypeModel = app.get< + typeof DelegationDelegationType + >(getModelToken(DelegationDelegationType)) + nationalRegistryApi = app.get(NationalRegistryClientService) + nationalRegistryV3Api = app.get(NationalRegistryV3ClientService) + const nationalRegistryV3FeatureService = app.get( + NationalRegistryV3FeatureService, + ) + jest + .spyOn(nationalRegistryV3FeatureService, 'getValue') + .mockImplementation(async () => featureFlag) + factory = new FixtureFactory(app) }) - await delegationDelegationTypeModel.create({ - delegationId: delegations.id, - delegationTypeId: AuthDelegationType.GeneralMandate, + afterAll(async () => { + await app.cleanUp() }) - await apiScopeModel.bulkCreate( - scopeNames.map((name) => ({ - name, - domainName: domain.name, - enabled: true, - description: `${name}: description`, - displayName: `${name}: display name`, - })), - ) - - // set 2 of 3 scopes to have general mandate delegation type - await apiScopeDelegationTypeModel.bulkCreate([ - { - apiScopeName: scopeNames[0], - delegationType: AuthDelegationType.GeneralMandate, - }, - { - apiScopeName: scopeNames[1], - delegationType: AuthDelegationType.GeneralMandate, - }, - ]) - - nationalRegistryApiSpy = jest - .spyOn(nationalRegistryApi, 'getIndividual') - .mockImplementation(async (id) => { - const user = createNationalRegistryUser({ - nationalId: representeeNationalId, + describe('GET with general mandate delegation type', () => { + const representeeNationalId = getFakeNationalId() + let nationalRegistryApiSpy: jest.SpyInstance + let nationalRegistryV3ApiSpy: jest.SpyInstance + const scopeNames = [ + 'api-scope/generalMandate1', + 'api-scope/generalMandate2', + 'api-scope/generalMandate3', + ] + + beforeAll(async () => { + client.supportedDelegationTypes = [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.LegalGuardian, + ] + await factory.createClient(client) + + const delegations = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Test', + fromNationalId: representeeNationalId, + toNationalId: userNationalId, + toName: 'Test', + }) + + await delegationProviderModel.create({ + id: AuthDelegationProvider.Custom, + name: 'Custom', + description: 'Custom', + }) + + await delegationDelegationTypeModel.create({ + delegationId: delegations.id, + delegationTypeId: AuthDelegationType.GeneralMandate, }) - return user ?? null + await apiScopeModel.bulkCreate( + scopeNames.map((name) => ({ + name, + domainName: domain.name, + enabled: true, + description: `${name}: description`, + displayName: `${name}: display name`, + })), + ) + + // set 2 of 3 scopes to have general mandate delegation type + await apiScopeDelegationTypeModel.bulkCreate([ + { + apiScopeName: scopeNames[0], + delegationType: AuthDelegationType.GeneralMandate, + }, + { + apiScopeName: scopeNames[1], + delegationType: AuthDelegationType.GeneralMandate, + }, + ]) + + nationalRegistryApiSpy = jest + .spyOn(nationalRegistryApi, 'getIndividual') + .mockImplementation(async (id) => { + const user = createNationalRegistryUser({ + nationalId: representeeNationalId, + }) + + return user ?? null + }) + + nationalRegistryV3ApiSpy = jest + .spyOn(nationalRegistryV3Api, 'getAllDataIndividual') + .mockImplementation(async () => { + const user = createNationalRegistryUser({ + nationalId: representeeNationalId, + }) + + return { kennitala: user.nationalId, nafn: user.name } + }) }) - }) - afterAll(async () => { - await app.cleanUp() - nationalRegistryApiSpy.mockClear() - }) + afterAll(async () => { + await app.cleanUp() + nationalRegistryApiSpy.mockClear() + nationalRegistryV3ApiSpy.mockClear() + }) - it('should return mergedDelegationDTO with the generalMandate', async () => { - const response = await server.get('/v2/delegations') + it('should return mergedDelegationDTO with the generalMandate', async () => { + const response = await server.get('/v2/delegations') - expect(response.status).toEqual(200) - expect(response.body).toHaveLength(1) - }) + expect(response.status).toEqual(200) + expect(response.body).toHaveLength(1) + }) - it('should get all general mandate scopes', async () => { - const response = await server.get('/delegations/scopes').query({ - fromNationalId: representeeNationalId, - delegationType: [AuthDelegationType.GeneralMandate], - }) + it('should get all general mandate scopes', async () => { + const response = await server.get('/delegations/scopes').query({ + fromNationalId: representeeNationalId, + delegationType: [AuthDelegationType.GeneralMandate], + }) - expect(response.status).toEqual(200) - expect(response.body).toEqual([scopeNames[0], scopeNames[1]]) - }) + expect(response.status).toEqual(200) + expect(response.body).toEqual([scopeNames[0], scopeNames[1]]) + }) - it('should only return valid general mandates', async () => { - const newNationalId = getFakeNationalId() - const newDelegation = await delegationModel.create({ - id: uuid(), - fromDisplayName: 'Test', - fromNationalId: newNationalId, - toNationalId: userNationalId, - toName: 'Test', - }) + it('should only return valid general mandates', async () => { + const newNationalId = getFakeNationalId() + const newDelegation = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Test', + fromNationalId: newNationalId, + toNationalId: userNationalId, + toName: 'Test', + }) - await delegationDelegationTypeModel.create({ - delegationId: newDelegation.id, - delegationTypeId: AuthDelegationType.GeneralMandate, - validTo: addDays(new Date(), -2), - }) + await delegationDelegationTypeModel.create({ + delegationId: newDelegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + validTo: addDays(new Date(), -2), + }) - const response = await server.get('/delegations/scopes').query({ - fromNationalId: newNationalId, - delegationType: [AuthDelegationType.GeneralMandate], - }) + const response = await server.get('/delegations/scopes').query({ + fromNationalId: newNationalId, + delegationType: [AuthDelegationType.GeneralMandate], + }) - expect(response.status).toEqual(200) - expect(response.body).toEqual([]) - }) + expect(response.status).toEqual(200) + expect(response.body).toEqual([]) + }) - it('should return all general mandate scopes and other preset scopes', async () => { - const newDelegation = await delegationModel.create({ - id: uuid(), - fromDisplayName: 'Test', - fromNationalId: representeeNationalId, - domainName: domain.name, - toNationalId: userNationalId, - toName: 'Test', - }) + it('should return all general mandate scopes and other preset scopes', async () => { + const newDelegation = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Test', + fromNationalId: representeeNationalId, + domainName: domain.name, + toNationalId: userNationalId, + toName: 'Test', + }) - await delegationTypeModel.create({ - id: AuthDelegationType.Custom, - name: 'custom', - description: 'custom', - providerId: AuthDelegationProvider.Custom, - }) + await delegationTypeModel.create({ + id: AuthDelegationType.Custom, + name: 'custom', + description: 'custom', + providerId: AuthDelegationProvider.Custom, + }) - await delegationScopesModel.create({ - id: uuid(), - delegationId: newDelegation.id, - scopeName: scopeNames[2], - // set valid from as yesterday and valid to as tomorrow - validFrom: addDays(new Date(), -1), - validTo: addDays(new Date(), 1), - }) + await delegationScopesModel.create({ + id: uuid(), + delegationId: newDelegation.id, + scopeName: scopeNames[2], + // set valid from as yesterday and valid to as tomorrow + validFrom: addDays(new Date(), -1), + validTo: addDays(new Date(), 1), + }) - await apiScopeDelegationTypeModel.create({ - apiScopeName: scopeNames[2], - delegationType: AuthDelegationType.LegalGuardian, - }) + await apiScopeDelegationTypeModel.create({ + apiScopeName: scopeNames[2], + delegationType: AuthDelegationType.LegalGuardian, + }) - const response = await server.get('/delegations/scopes').query({ - fromNationalId: representeeNationalId, - delegationType: [ - AuthDelegationType.GeneralMandate, - AuthDelegationType.LegalGuardian, - ], - }) + const response = await server.get('/delegations/scopes').query({ + fromNationalId: representeeNationalId, + delegationType: [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.LegalGuardian, + ], + }) - expect(response.status).toEqual(200) - expect(response.body).toEqual(expect.arrayContaining(scopeNames)) - expect(response.body).toHaveLength(scopeNames.length) + expect(response.status).toEqual(200) + expect(response.body).toEqual(expect.arrayContaining(scopeNames)) + expect(response.body).toHaveLength(scopeNames.length) + }) + }) }) - }) - }) + }, + ) }) diff --git a/apps/services/auth/personal-representative/infra/personal-representative.ts b/apps/services/auth/personal-representative/infra/personal-representative.ts index 180b75ec9891..42928e86db9f 100644 --- a/apps/services/auth/personal-representative/infra/personal-representative.ts +++ b/apps/services/auth/personal-representative/infra/personal-representative.ts @@ -1,5 +1,10 @@ import { json, service, ServiceBuilder } from '../../../../../infra/src/dsl/dsl' -import { Base, Client, RskProcuring } from '../../../../../infra/src/dsl/xroad' +import { + Base, + Client, + NationalRegistryAuthB2C, + RskProcuring, +} from '../../../../../infra/src/dsl/xroad' const REDIS_NODE_CONFIG = { dev: json([ @@ -62,8 +67,10 @@ export const serviceSetup = '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET', SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME', SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD', + NATIONAL_REGISTRY_B2C_CLIENT_SECRET: + '/k8s/services-auth/NATIONAL_REGISTRY_B2C_CLIENT_SECRET', }) - .xroad(Base, Client, RskProcuring) + .xroad(Base, Client, RskProcuring, NationalRegistryAuthB2C) .ingress({ primary: { host: { diff --git a/apps/services/auth/personal-representative/src/app/app.module.ts b/apps/services/auth/personal-representative/src/app/app.module.ts index e46d8b36eeed..0c89f4dd3a5a 100644 --- a/apps/services/auth/personal-representative/src/app/app.module.ts +++ b/apps/services/auth/personal-representative/src/app/app.module.ts @@ -8,8 +8,10 @@ import { import { AuthModule } from '@island.is/auth-nest-tools' import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientConfig } from '@island.is/clients/national-registry-v3' import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' import { SyslumennClientConfig } from '@island.is/clients/syslumenn' +import { ZendeskServiceConfig } from '@island.is/clients/zendesk' import { AuditModule } from '@island.is/nest/audit' import { ConfigModule, @@ -17,12 +19,11 @@ import { XRoadConfig, } from '@island.is/nest/config' import { FeatureFlagConfig } from '@island.is/nest/feature-flags' -import { ZendeskServiceConfig } from '@island.is/clients/zendesk' import { environment } from '../environments' -import { PersonalRepresentativeTypesModule } from './modules/personalRepresentativeTypes/personalRepresentativeTypes.module' import { AccessLogsModule } from './modules/accessLogs/accessLogs.module' import { PersonalRepresentativesModule } from './modules/personalRepresentatives/personalRepresentatives.module' +import { PersonalRepresentativeTypesModule } from './modules/personalRepresentativeTypes/personalRepresentativeTypes.module' import { RightTypesModule } from './modules/rightTypes/rightTypes.module' @Module({ @@ -38,6 +39,7 @@ import { RightTypesModule } from './modules/rightTypes/rightTypes.module' DelegationConfig, IdsClientConfig, NationalRegistryClientConfig, + NationalRegistryV3ClientConfig, RskRelationshipsClientConfig, CompanyRegistryConfig, XRoadConfig, diff --git a/apps/services/auth/public-api/infra/auth-public-api.ts b/apps/services/auth/public-api/infra/auth-public-api.ts index b27663150b40..609588c46271 100644 --- a/apps/services/auth/public-api/infra/auth-public-api.ts +++ b/apps/services/auth/public-api/infra/auth-public-api.ts @@ -1,5 +1,10 @@ import { json, service, ServiceBuilder } from '../../../../../infra/src/dsl/dsl' -import { Base, Client, RskProcuring } from '../../../../../infra/src/dsl/xroad' +import { + Base, + Client, + NationalRegistryAuthB2C, + RskProcuring, +} from '../../../../../infra/src/dsl/xroad' const REDIS_NODE_CONFIG = { dev: json([ @@ -86,8 +91,10 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-public-api'> => { '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET', SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME', SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD', + NATIONAL_REGISTRY_B2C_CLIENT_SECRET: + '/k8s/services-auth/NATIONAL_REGISTRY_B2C_CLIENT_SECRET', }) - .xroad(Base, Client, RskProcuring) + .xroad(Base, Client, RskProcuring, NationalRegistryAuthB2C) .ingress({ primary: { host: { diff --git a/apps/services/auth/public-api/src/app/app.module.ts b/apps/services/auth/public-api/src/app/app.module.ts index e3719da7e4b9..7fe7ba43de60 100644 --- a/apps/services/auth/public-api/src/app/app.module.ts +++ b/apps/services/auth/public-api/src/app/app.module.ts @@ -9,8 +9,10 @@ import { import { AuthModule } from '@island.is/auth-nest-tools' import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientConfig } from '@island.is/clients/national-registry-v3' import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' import { SyslumennClientConfig } from '@island.is/clients/syslumenn' +import { ZendeskServiceConfig } from '@island.is/clients/zendesk' import { AuditModule } from '@island.is/nest/audit' import { ConfigModule, @@ -23,7 +25,6 @@ import { ProblemModule } from '@island.is/nest/problem' import { environment } from '../environments' import { DelegationsModule } from './modules/delegations/delegations.module' import { PasskeysModule } from './modules/passkeys/passkeys.module' -import { ZendeskServiceConfig } from '@island.is/clients/zendesk' @Module({ imports: [ @@ -42,6 +43,7 @@ import { ZendeskServiceConfig } from '@island.is/clients/zendesk' FeatureFlagConfig, IdsClientConfig, NationalRegistryClientConfig, + NationalRegistryV3ClientConfig, RskRelationshipsClientConfig, CompanyRegistryConfig, XRoadConfig, diff --git a/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts b/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts index d57f16dea37d..a7ad9fb47b1e 100644 --- a/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts +++ b/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts @@ -1,8 +1,8 @@ import { getModelToken } from '@nestjs/sequelize' +import addYears from 'date-fns/addYears' +import kennitala from 'kennitala' import times from 'lodash/times' import request from 'supertest' -import kennitala from 'kennitala' -import addYears from 'date-fns/addYears' import { ClientDelegationType, @@ -14,6 +14,7 @@ import { DelegationScope, DelegationTypeModel, MergedDelegationDTO, + NationalRegistryV3FeatureService, PersonalRepresentative, PersonalRepresentativeDelegationTypeModel, PersonalRepresentativeRight, @@ -26,6 +27,7 @@ import { IndividualDto, NationalRegistryClientService, } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientService } from '@island.is/clients/national-registry-v3' import { createClient, createDelegation, @@ -153,923 +155,976 @@ beforeAll(() => { }) describe('ActorDelegationsController', () => { - describe('with auth', () => { - let app: TestApp - let server: request.SuperTest - let delegationModel: typeof Delegation - let delegationDelegationTypeModel: typeof DelegationDelegationType - let clientDelegationTypeModel: typeof ClientDelegationType - let nationalRegistryApi: NationalRegistryClientService - - beforeAll(async () => { - // TestApp setup with auth and database - app = await setupWithAuth({ - user, - userName, - nationalRegistryUser, - client: { - props: client, - scopes: Scopes.slice(0, 4).map((s) => s.name), - }, - }) - server = request(app.getHttpServer()) - - // Get reference on Delegation and Client models to seed DB - delegationModel = app.get(getModelToken(Delegation)) - clientDelegationTypeModel = app.get( - getModelToken(ClientDelegationType), - ) - delegationDelegationTypeModel = app.get( - getModelToken(DelegationDelegationType), - ) - nationalRegistryApi = app.get(NationalRegistryClientService) - }) - - beforeEach(async () => { - return await clientDelegationTypeModel.bulkCreate( - delegationTypes.map((type) => ({ - clientId: client.clientId, - delegationType: type, - })), - { - updateOnDuplicate: ['modified'], - }, - ) - }) - - afterAll(async () => { - await app.cleanUp() - }) - - afterEach(async () => { - await delegationModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - }) - - describe('GET /actor/delegations', () => { - let nationalRegistryApiSpy: jest.SpyInstance - const path = '/v1/actor/delegations' - const query = '?direction=incoming' - const deceasedNationalIds = times(3, () => createNationalId('person')) - const nationalRegistryUsers = [ - nationalRegistryUser, - ...Object.values(mockDelegations).map((delegation) => - createNationalRegistryUser({ - nationalId: delegation.fromNationalId, - }), - ), - ...times(10, () => - createNationalRegistryUser({ - name: getFakeName(), - nationalId: createNationalId('person'), - }), - ), - ] - - beforeAll(async () => { - nationalRegistryApiSpy = jest - .spyOn(nationalRegistryApi, 'getIndividual') - .mockImplementation(async (id) => { - if (deceasedNationalIds.includes(id)) { - return null - } + describe.each([false, true])( + 'national registry v3 featureflag: %s', + (featureFlag) => { + describe('with auth', () => { + let app: TestApp + let server: request.SuperTest + let delegationModel: typeof Delegation + let delegationDelegationTypeModel: typeof DelegationDelegationType + let clientDelegationTypeModel: typeof ClientDelegationType + let nationalRegistryApi: NationalRegistryClientService + let nationalRegistryV3Api: NationalRegistryV3ClientService - const user = nationalRegistryUsers.find((u) => u?.nationalId === id) - - return user ?? null + beforeAll(async () => { + // TestApp setup with auth and database + app = await setupWithAuth({ + user, + userName, + nationalRegistryUser, + client: { + props: client, + scopes: Scopes.slice(0, 4).map((s) => s.name), + }, }) - }) - - it('should return only valid delegations', async () => { - // Arrange - await createDelegationModels( - delegationModel, - Object.values(mockDelegations), - ) - const expectedModels = await findExpectedMergedDelegationModels( - delegationModel, - [ - mockDelegations.incoming.id, - mockDelegations.incomingBothValidAndNotAllowed.id, - mockDelegations.incomingWithOtherDomain.id, - ], - [Scopes[0].name], - ) - - // Act - const res = await server.get(`${path}${query}`) - - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(3) - expectMatchingMergedDelegations( - res.body, - updateDelegationFromNameToPersonName( - expectedModels, - nationalRegistryUsers, - ), - ) - }) + server = request(app.getHttpServer()) - it('should return only delegations with scopes the client has access to', async () => { - // Arrange - await createDelegationModels(delegationModel, [ - mockDelegations.incomingWithOtherDomain, - ]) - const expectedModel = await findExpectedMergedDelegationModels( - delegationModel, - mockDelegations.incomingWithOtherDomain.id, - [Scopes[0].name], - ) - - // Act - const res = await server.get(`${path}${query}`) - - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expectMatchingMergedDelegations( - res.body[0], - updateDelegationFromNameToPersonName( - expectedModel, - nationalRegistryUsers, - ), - ) - }) - - it('should return custom delegations and general mandate when the delegationTypes filter has both types and delegation exists for both', async () => { - // Arrange - const delegation = createDelegation({ - fromNationalId: nationalRegistryUser.nationalId, - toNationalId: user.nationalId, - scopes: [], + // Get reference on Delegation and Client models to seed DB + delegationModel = app.get( + getModelToken(Delegation), + ) + clientDelegationTypeModel = app.get( + getModelToken(ClientDelegationType), + ) + delegationDelegationTypeModel = app.get< + typeof DelegationDelegationType + >(getModelToken(DelegationDelegationType)) + nationalRegistryApi = app.get(NationalRegistryClientService) + nationalRegistryV3Api = app.get(NationalRegistryV3ClientService) }) - await delegationModel.create(delegation) - - await delegationDelegationTypeModel.create({ - delegationId: delegation.id, - delegationTypeId: AuthDelegationType.GeneralMandate, + beforeEach(async () => { + return await clientDelegationTypeModel.bulkCreate( + delegationTypes.map((type) => ({ + clientId: client.clientId, + delegationType: type, + })), + { + updateOnDuplicate: ['modified'], + }, + ) }) - await createDelegationModels(delegationModel, [ - mockDelegations.incomingWithOtherDomain, - ]) - - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.GeneralMandate}`, - ) - - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(2) - expect( - res.body - .map((d: MergedDelegationDTO) => d.types) - .flat() - .sort(), - ).toEqual( - [AuthDelegationType.Custom, AuthDelegationType.GeneralMandate].sort(), - ) - }) - - it('should return a merged object with both Custom and GeneralMandate types', async () => { - // Arrange - const delegation = createDelegation({ - fromNationalId: - mockDelegations.incomingWithOtherDomain.fromNationalId, - toNationalId: user.nationalId, - domainName: null, - scopes: [], + afterAll(async () => { + await app.cleanUp() }) - await delegationModel.create(delegation) - - await delegationDelegationTypeModel.create({ - delegationId: delegation.id, - delegationTypeId: AuthDelegationType.GeneralMandate, + afterEach(async () => { + await delegationModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) }) - await createDelegationModels(delegationModel, [ - mockDelegations.incomingWithOtherDomain, - ]) - - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.GeneralMandate}`, - ) - - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expect( - res.body - .map((d: MergedDelegationDTO) => d.types) - .flat() - .sort(), - ).toEqual( - [AuthDelegationType.Custom, AuthDelegationType.GeneralMandate].sort(), - ) - }) - - it('should return only delegations related to the provided otherUser national id', async () => { - // Arrange - await createDelegationModels(delegationModel, [ - mockDelegations.incoming, - ]) - const expectedModel = await findExpectedMergedDelegationModels( - delegationModel, - mockDelegations.incoming.id, - [Scopes[0].name], - ) - - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&otherUser=${mockDelegations.incoming.fromNationalId}`, - ) - - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expectMatchingMergedDelegations( - res.body[0], - updateDelegationFromNameToPersonName( - expectedModel, - nationalRegistryUsers, - ), - ) - }) - - it('should return only delegations related to the provided otherUser national id without the general mandate since there is none', async () => { - // Arrange - await createDelegationModels(delegationModel, [ - mockDelegations.incoming, - ]) - const expectedModel = await findExpectedMergedDelegationModels( - delegationModel, - mockDelegations.incoming.id, - [Scopes[0].name], - ) - - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes${AuthDelegationType.GeneralMandate}&otherUser=${mockDelegations.incoming.fromNationalId}`, - ) - - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expectMatchingMergedDelegations( - res.body[0], - updateDelegationFromNameToPersonName( - expectedModel, - nationalRegistryUsers, - ), - ) - }) - - it('should return empty array when provided otherUser national id is not related to any delegation', async () => { - // Arrange - const unrelatedNationalId = createNationalId('person') + describe('GET /actor/delegations', () => { + let nationalRegistryApiSpy: jest.SpyInstance + let nationalRegistryV3ApiSpy: jest.SpyInstance + const path = '/v1/actor/delegations' + const query = '?direction=incoming' + const deceasedNationalIds = times(3, () => createNationalId('person')) + const nationalRegistryUsers = [ + nationalRegistryUser, + ...Object.values(mockDelegations).map((delegation) => + createNationalRegistryUser({ + nationalId: delegation.fromNationalId, + }), + ), + ...times(10, () => + createNationalRegistryUser({ + name: getFakeName(), + nationalId: createNationalId('person'), + }), + ), + ] - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&otherUser=${unrelatedNationalId}`, - ) - - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) - - it('should return custom delegations and not deceased delegations, when the delegationTypes filter is custom type', async () => { - // Arrange - await createDelegationModels(delegationModel, [ - mockDelegations.incoming, - createDelegation({ - fromNationalId: deceasedNationalIds[0], - toNationalId: user.nationalId, - scopes: [Scopes[0].name], - today, - }), - createDelegation({ - fromNationalId: deceasedNationalIds[1], - toNationalId: user.nationalId, - scopes: [Scopes[0].name], - today, - }), - ]) - - // We expect the first model to be returned, but not the second or third since they are tied to a deceased person - const expectedModel = await findExpectedMergedDelegationModels( - delegationModel, - mockDelegations.incoming.id, - [Scopes[0].name], - ) + beforeAll(async () => { + nationalRegistryApiSpy = jest + .spyOn(nationalRegistryApi, 'getIndividual') + .mockImplementation(async (id) => { + if (deceasedNationalIds.includes(id)) { + return null + } + + const user = nationalRegistryUsers.find( + (u) => u?.nationalId === id, + ) + + return user ?? null + }) + const nationalRegistryV3FeatureService = app.get( + NationalRegistryV3FeatureService, + ) + jest + .spyOn(nationalRegistryV3FeatureService, 'getValue') + .mockImplementation(async () => featureFlag) + nationalRegistryV3ApiSpy = jest + .spyOn(nationalRegistryV3Api, 'getAllDataIndividual') + .mockImplementation(async (id) => { + if (deceasedNationalIds.includes(id)) { + return { + kennitala: id, + afdrif: 'LÉST', + } + } + + const user = nationalRegistryUsers.find( + (u) => u?.nationalId === id, + ) + + return user + ? { + kennitala: id, + nafn: user?.name, + afdrif: null, + } + : null + }) + }) - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}`, - ) + it('should return only valid delegations', async () => { + // Arrange + await createDelegationModels( + delegationModel, + Object.values(mockDelegations), + ) + const expectedModels = await findExpectedMergedDelegationModels( + delegationModel, + [ + mockDelegations.incoming.id, + mockDelegations.incomingBothValidAndNotAllowed.id, + mockDelegations.incomingWithOtherDomain.id, + ], + [Scopes[0].name], + ) + + // Act + const res = await server.get(`${path}${query}`) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(3) + expectMatchingMergedDelegations( + res.body, + updateDelegationFromNameToPersonName( + expectedModels, + nationalRegistryUsers, + ), + ) + }) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expectMatchingMergedDelegations( - res.body[0], - updateDelegationFromNameToPersonName( - expectedModel, - nationalRegistryUsers, - ), - ) - - // Verify - const expectedModifiedModels = await delegationModel.findAll({ - where: { - toNationalId: user.nationalId, - }, - include: [ - { - model: DelegationScope, - as: 'delegationScopes', - }, - ], - }) + it('should return only delegations with scopes the client has access to', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incomingWithOtherDomain, + ]) + const expectedModel = await findExpectedMergedDelegationModels( + delegationModel, + mockDelegations.incomingWithOtherDomain.id, + [Scopes[0].name], + ) + + // Act + const res = await server.get(`${path}${query}`) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expectMatchingMergedDelegations( + res.body[0], + updateDelegationFromNameToPersonName( + expectedModel, + nationalRegistryUsers, + ), + ) + }) - expect(expectedModifiedModels.length).toEqual(1) - }) + it('should return custom delegations and general mandate when the delegationTypes filter has both types and delegation exists for both', async () => { + // Arrange + const delegation = createDelegation({ + fromNationalId: nationalRegistryUser.nationalId, + toNationalId: user.nationalId, + scopes: [], + }) - it('should not mix up companies and individuals when processing deceased delegations [BUG]', async () => { - // Arrange - const incomingCompany = createDelegation({ - fromNationalId: createNationalId('company'), - toNationalId: user.nationalId, - scopes: [Scopes[0].name], - today, - }) - await createDelegationModels(delegationModel, [ - // The order of these is important to trigger the previous bug. - incomingCompany, - mockDelegations.incoming, - ]) - - // We expect both models to be returned. - const expectedModels = await findExpectedMergedDelegationModels( - delegationModel, - [mockDelegations.incoming.id, incomingCompany.id], - [Scopes[0].name], - ) + await delegationModel.create(delegation) - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}`, - ) + await delegationDelegationTypeModel.create({ + delegationId: delegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(2) - expectMatchingMergedDelegations( - res.body, - updateDelegationFromNameToPersonName( - expectedModels, - nationalRegistryUsers, - ), - ) - }) + await createDelegationModels(delegationModel, [ + mockDelegations.incomingWithOtherDomain, + ]) - it('should return delegations which only has scopes with special scope rules [BUG]', async () => { - // Arrange - const specialDelegation = createDelegation({ - fromNationalId: nationalRegistryUser.nationalId, - toNationalId: user.nationalId, - scopes: [Scopes[3].name], - today, - }) - await createDelegationModels(delegationModel, [specialDelegation]) + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.GeneralMandate}`, + ) - // We expect the delegation to be returned. - const expectedModels = await findExpectedMergedDelegationModels( - delegationModel, - [specialDelegation.id], - ) + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(2) + expect( + res.body + .map((d: MergedDelegationDTO) => d.types) + .flat() + .sort(), + ).toEqual( + [ + AuthDelegationType.Custom, + AuthDelegationType.GeneralMandate, + ].sort(), + ) + }) - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}`, - ) + it('should return a merged object with both Custom and GeneralMandate types', async () => { + // Arrange + const delegation = createDelegation({ + fromNationalId: + mockDelegations.incomingWithOtherDomain.fromNationalId, + toNationalId: user.nationalId, + domainName: null, + scopes: [], + }) - // Assert - expect(res.status).toEqual(200) - expectMatchingMergedDelegations( - res.body, - updateDelegationFromNameToPersonName( - expectedModels, - nationalRegistryUsers, - ), - ) - }) + await delegationModel.create(delegation) - it('should return delegations when the delegationTypes filter is empty', async () => { - // Arrange - await createDelegationModels(delegationModel, [ - mockDelegations.incomingWithOtherDomain, - ]) - const expectedModel = await findExpectedMergedDelegationModels( - delegationModel, - mockDelegations.incomingWithOtherDomain.id, - [Scopes[0].name], - ) + await delegationDelegationTypeModel.create({ + delegationId: delegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) - // Act - const res = await server.get(`${path}${query}&delegationTypes=`) + await createDelegationModels(delegationModel, [ + mockDelegations.incomingWithOtherDomain, + ]) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expectMatchingMergedDelegations( - res.body[0], - updateDelegationFromNameToPersonName( - expectedModel, - nationalRegistryUsers, - ), - ) - }) + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.GeneralMandate}`, + ) - it('should not return custom delegations when the delegationTypes filter does not include custom type', async () => { - // Arrange - await createDelegationModels(delegationModel, [ - mockDelegations.incomingWithOtherDomain, - ]) - - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.ProcurationHolder}`, - ) + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expect( + res.body + .map((d: MergedDelegationDTO) => d.types) + .flat() + .sort(), + ).toEqual( + [ + AuthDelegationType.Custom, + AuthDelegationType.GeneralMandate, + ].sort(), + ) + }) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) + it('should return only delegations related to the provided otherUser national id', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incoming, + ]) + const expectedModel = await findExpectedMergedDelegationModels( + delegationModel, + mockDelegations.incoming.id, + [Scopes[0].name], + ) + + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&otherUser=${mockDelegations.incoming.fromNationalId}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expectMatchingMergedDelegations( + res.body[0], + updateDelegationFromNameToPersonName( + expectedModel, + nationalRegistryUsers, + ), + ) + }) - it('should return no delegation when the client does not have access to any scope', async () => { - // Arrange - await createDelegationModels(delegationModel, [ - mockDelegations.incomingOnlyOtherDomain, - ]) + it('should return only delegations related to the provided otherUser national id without the general mandate since there is none', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incoming, + ]) + const expectedModel = await findExpectedMergedDelegationModels( + delegationModel, + mockDelegations.incoming.id, + [Scopes[0].name], + ) + + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes${AuthDelegationType.GeneralMandate}&otherUser=${mockDelegations.incoming.fromNationalId}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expectMatchingMergedDelegations( + res.body[0], + updateDelegationFromNameToPersonName( + expectedModel, + nationalRegistryUsers, + ), + ) + }) - // Act - const res = await server.get(`${path}${query}`) + it('should return empty array when provided otherUser national id is not related to any delegation', async () => { + // Arrange + const unrelatedNationalId = createNationalId('person') - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&otherUser=${unrelatedNationalId}`, + ) - it('should return 400 BadRequest if required query paramter is missing', async () => { - // Act - const res = await server.get(path) + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) + }) - // Assert - expect(res.status).toEqual(400) - expect(res.body).toMatchObject({ - status: 400, - type: 'https://httpstatuses.org/400', - title: 'Bad Request', - detail: - "'direction' can only be set to incoming for the /actor alias", - }) - }) + it('should return custom delegations and not deceased delegations, when the delegationTypes filter is custom type', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incoming, + createDelegation({ + fromNationalId: deceasedNationalIds[0], + toNationalId: user.nationalId, + scopes: [Scopes[0].name], + today, + }), + createDelegation({ + fromNationalId: deceasedNationalIds[1], + toNationalId: user.nationalId, + scopes: [Scopes[0].name], + today, + }), + ]) + + // We expect the first model to be returned, but not the second or third since they are tied to a deceased person + const expectedModel = await findExpectedMergedDelegationModels( + delegationModel, + mockDelegations.incoming.id, + [Scopes[0].name], + ) + + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expectMatchingMergedDelegations( + res.body[0], + updateDelegationFromNameToPersonName( + expectedModel, + nationalRegistryUsers, + ), + ) + + // Verify + const expectedModifiedModels = await delegationModel.findAll({ + where: { + toNationalId: user.nationalId, + }, + include: [ + { + model: DelegationScope, + as: 'delegationScopes', + }, + ], + }) - it('should not return delegation with no scope longer allowed for delegation', async () => { - // Arrange - await createDelegationModels(delegationModel, [ - mockDelegations.incomingWithNoAllowed, - ]) + expect(expectedModifiedModels.length).toEqual(1) + }) - // Act - const res = await server.get(`${path}${query}`) + it('should not mix up companies and individuals when processing deceased delegations [BUG]', async () => { + // Arrange + const incomingCompany = createDelegation({ + fromNationalId: createNationalId('company'), + toNationalId: user.nationalId, + scopes: [Scopes[0].name], + today, + }) + await createDelegationModels(delegationModel, [ + // The order of these is important to trigger the previous bug. + incomingCompany, + mockDelegations.incoming, + ]) + + // We expect both models to be returned. + const expectedModels = await findExpectedMergedDelegationModels( + delegationModel, + [mockDelegations.incoming.id, incomingCompany.id], + [Scopes[0].name], + ) + + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(2) + expectMatchingMergedDelegations( + res.body, + updateDelegationFromNameToPersonName( + expectedModels, + nationalRegistryUsers, + ), + ) + }) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) + it('should return delegations which only has scopes with special scope rules [BUG]', async () => { + // Arrange + const specialDelegation = createDelegation({ + fromNationalId: nationalRegistryUser.nationalId, + toNationalId: user.nationalId, + scopes: [Scopes[3].name], + today, + }) + await createDelegationModels(delegationModel, [specialDelegation]) + + // We expect the delegation to be returned. + const expectedModels = await findExpectedMergedDelegationModels( + delegationModel, + [specialDelegation.id], + ) + + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}`, + ) + + // Assert + expect(res.status).toEqual(200) + expectMatchingMergedDelegations( + res.body, + updateDelegationFromNameToPersonName( + expectedModels, + nationalRegistryUsers, + ), + ) + }) - it('should not return delegation when client does not support custom delegations', async () => { - // Arrange - await createDelegationModels(delegationModel, [ - mockDelegations.incoming, - ]) - await clientDelegationTypeModel.destroy({ - where: { - clientId: client.clientId, - delegationType: AuthDelegationType.Custom, - }, - }) + it('should return delegations when the delegationTypes filter is empty', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incomingWithOtherDomain, + ]) + const expectedModel = await findExpectedMergedDelegationModels( + delegationModel, + mockDelegations.incomingWithOtherDomain.id, + [Scopes[0].name], + ) + + // Act + const res = await server.get(`${path}${query}&delegationTypes=`) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expectMatchingMergedDelegations( + res.body[0], + updateDelegationFromNameToPersonName( + expectedModel, + nationalRegistryUsers, + ), + ) + }) - // Act - const res = await server.get(`${path}${query}`) + it('should not return custom delegations when the delegationTypes filter does not include custom type', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incomingWithOtherDomain, + ]) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.ProcurationHolder}`, + ) - describe('with legal guardian delegations', () => { - let getForsja: jest.SpyInstance - let clientInstance: any - - const mockForKt = (kt: string): void => { - jest.spyOn(kennitala, 'info').mockReturnValue({ - kt, - age: 16, - birthday: addYears(Date.now(), -15), - birthdayReadable: '', - type: 'person', - valid: true, + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) }) - jest.spyOn(clientInstance, 'getIndividual').mockResolvedValueOnce({ - nationalId: kt, - name: nationalRegistryUser.name, - } as IndividualDto) - - jest - .spyOn(clientInstance, 'getCustodyChildren') - .mockResolvedValueOnce([kt]) - } - - beforeEach(() => { - clientInstance = app.get(NationalRegistryClientService) - getForsja = jest - .spyOn(clientInstance, 'getCustodyChildren') - .mockResolvedValue([nationalRegistryUser.nationalId]) - }) - - afterAll(() => { - getForsja.mockRestore() - }) + it('should return no delegation when the client does not have access to any scope', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incomingOnlyOtherDomain, + ]) - it('should return delegations', async () => { - const kt = '1111089030' - - // Arrange - mockForKt(kt) - - const expectedDelegation = DelegationDTOMapper.toMergedDelegationDTO({ - fromName: nationalRegistryUser.name, - fromNationalId: kt, - provider: AuthDelegationProvider.NationalRegistry, - toNationalId: user.nationalId, - type: [ - AuthDelegationType.LegalGuardian, - AuthDelegationType.LegalGuardianMinor, - ], - } as Omit & { type: AuthDelegationType | AuthDelegationType[] }) - - // Act - const res = await server.get(`${path}${query}`) - - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expect(res.body[0]).toEqual(expectedDelegation) - }) + // Act + const res = await server.get(`${path}${query}`) - it('should not return delegations when client does not support legal guardian delegations', async () => { - await clientDelegationTypeModel.destroy({ - where: { - clientId: client.clientId, - delegationType: AuthDelegationType.LegalGuardian, - }, + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) }) - // Act - const res = await server.get(`${path}${query}`) - - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) - - it('should return a legal guardian delegation since the type is included in the delegationTypes filter', async () => { - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.LegalGuardian}`, - ) + it('should return 400 BadRequest if required query paramter is missing', async () => { + // Act + const res = await server.get(path) + + // Assert + expect(res.status).toEqual(400) + expect(res.body).toMatchObject({ + status: 400, + type: 'https://httpstatuses.org/400', + title: 'Bad Request', + detail: + "'direction' can only be set to incoming for the /actor alias", + }) + }) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expect(res.body[0].types[0]).toEqual(AuthDelegationType.LegalGuardian) - }) + it('should not return delegation with no scope longer allowed for delegation', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incomingWithNoAllowed, + ]) - it('should not return a legal guardian delegation since the type is not included in the delegationTypes filter', async () => { - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}`, - ) + // Act + const res = await server.get(`${path}${query}`) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) - }) + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) + }) - describe('with procuring delegations', () => { - let getIndividualRelationships: jest.SpyInstance - beforeAll(() => { - const client = app.get(RskRelationshipsClient) - getIndividualRelationships = jest - .spyOn(client, 'getIndividualRelationships') - .mockResolvedValue({ - name: nationalRegistryUser.name, - nationalId: nationalRegistryUser.nationalId, - relationships: [ - { - nationalId: nationalRegistryUser.nationalId, - name: nationalRegistryUser.name, - }, - ], + it('should not return delegation when client does not support custom delegations', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incoming, + ]) + await clientDelegationTypeModel.destroy({ + where: { + clientId: client.clientId, + delegationType: AuthDelegationType.Custom, + }, }) - }) - afterAll(() => { - getIndividualRelationships.mockRestore() - }) + // Act + const res = await server.get(`${path}${query}`) - it('should return delegations', async () => { - // Arrange - const expectedDelegation = { - fromName: nationalRegistryUser.name, - fromNationalId: nationalRegistryUser.nationalId, - provider: 'fyrirtaekjaskra', - toNationalId: user.nationalId, - type: 'ProcurationHolder', - } as DelegationDTO - // Act - const res = await server.get(`${path}${query}`) - - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expect(res.body[0]).toEqual( - DelegationDTOMapper.toMergedDelegationDTO(expectedDelegation), - ) - }) - - it('should not return delegations when client does not support procuring holder delegations', async () => { - // Arrange - await clientDelegationTypeModel.destroy({ - where: { - clientId: client.clientId, - delegationType: AuthDelegationType.ProcurationHolder, - }, + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) }) - // Act - const res = await server.get(`${path}${query}`) + describe('with legal guardian delegations', () => { + let getForsja: jest.SpyInstance + let clientInstance: any + + const mockForKt = (kt: string): void => { + jest.spyOn(kennitala, 'info').mockReturnValue({ + kt, + age: 16, + birthday: addYears(Date.now(), -15), + birthdayReadable: '', + type: 'person', + valid: true, + }) + + jest + .spyOn(clientInstance, 'getIndividual') + .mockResolvedValueOnce({ + nationalId: kt, + name: nationalRegistryUser.name, + } as IndividualDto) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) + jest + .spyOn(clientInstance, 'getCustodyChildren') + .mockResolvedValueOnce([kt]) + } - it('should return a procuring holder delegation since the type is included in the delegationTypes filter', async () => { - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.ProcurationHolder}&delegationTypes=${AuthDelegationType.PersonalRepresentative}`, - ) + beforeEach(() => { + clientInstance = app.get(NationalRegistryClientService) + getForsja = jest + .spyOn(clientInstance, 'getCustodyChildren') + .mockResolvedValue([nationalRegistryUser.nationalId]) + }) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expect(res.body[0].types[0]).toEqual( - AuthDelegationType.ProcurationHolder, - ) - }) + afterAll(() => { + getForsja.mockRestore() + }) - it('should not return a procuring holder delegation since the type is not included in the delegationTypes filter', async () => { - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.LegalGuardian}&delegationTypes=${AuthDelegationType.PersonalRepresentative}`, - ) + it('should return delegations', async () => { + const kt = '1111089030' + + // Arrange + mockForKt(kt) + + const expectedDelegation = + DelegationDTOMapper.toMergedDelegationDTO({ + fromName: nationalRegistryUser.name, + fromNationalId: kt, + provider: AuthDelegationProvider.NationalRegistry, + toNationalId: user.nationalId, + type: [ + AuthDelegationType.LegalGuardian, + AuthDelegationType.LegalGuardianMinor, + ], + } as Omit & { type: AuthDelegationType | AuthDelegationType[] }) + + // Act + const res = await server.get(`${path}${query}`) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expect(res.body[0]).toEqual(expectedDelegation) + }) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) - }) + it('should not return delegations when client does not support legal guardian delegations', async () => { + await clientDelegationTypeModel.destroy({ + where: { + clientId: client.clientId, + delegationType: AuthDelegationType.LegalGuardian, + }, + }) - describe('when user is a personal representative with one representee', () => { - let prModel: typeof PersonalRepresentative - let prRightsModel: typeof PersonalRepresentativeRight - let prRightTypeModel: typeof PersonalRepresentativeRightType - let prTypeModel: typeof PersonalRepresentativeType - let delegationTypeModel: typeof DelegationTypeModel - let delegationProviderModel: typeof DelegationProviderModel - let prDelegationTypeModel: typeof PersonalRepresentativeDelegationTypeModel + // Act + const res = await server.get(`${path}${query}`) - beforeAll(async () => { - prTypeModel = app.get( - 'PersonalRepresentativeTypeRepository', - ) - prModel = app.get( - 'PersonalRepresentativeRepository', - ) - prRightTypeModel = app.get( - 'PersonalRepresentativeRightTypeRepository', - ) - prRightsModel = app.get( - 'PersonalRepresentativeRightRepository', - ) - delegationTypeModel = app.get( - getModelToken(DelegationTypeModel), - ) - delegationProviderModel = app.get( - getModelToken(DelegationProviderModel), - ) - prDelegationTypeModel = app.get< - typeof PersonalRepresentativeDelegationTypeModel - >(getModelToken(PersonalRepresentativeDelegationTypeModel)) - - const prType = await prTypeModel.create({ - code: 'prTypeCode', - name: 'prTypeName', - description: 'prTypeDescription', - }) + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) + }) - const pr = await prModel.create({ - nationalIdPersonalRepresentative: user.nationalId, - nationalIdRepresentedPerson: nationalRegistryUser.nationalId, - personalRepresentativeTypeCode: prType.code, - contractId: '1', - externalUserId: '1', - }) + it('should return a legal guardian delegation since the type is included in the delegationTypes filter', async () => { + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.LegalGuardian}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expect(res.body[0].types[0]).toEqual( + AuthDelegationType.LegalGuardian, + ) + }) - const dt = await delegationTypeModel.create({ - id: getPersonalRepresentativeDelegationType('prRightType'), - providerId: AuthDelegationProvider.PersonalRepresentativeRegistry, - name: `Personal Representative: prRightType`, - description: `Personal representative delegation type for right type prRightType`, - }) + it('should not return a legal guardian delegation since the type is not included in the delegationTypes filter', async () => { + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}`, + ) - const prRightType = await prRightTypeModel.create({ - code: 'prRightType', - description: 'prRightTypeDescription', + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) + }) }) - const prr = await prRightsModel.create({ - rightTypeCode: prRightType.code, - personalRepresentativeId: pr.id, - }) + describe('with procuring delegations', () => { + let getIndividualRelationships: jest.SpyInstance + beforeAll(() => { + const client = app.get(RskRelationshipsClient) + getIndividualRelationships = jest + .spyOn(client, 'getIndividualRelationships') + .mockResolvedValue({ + name: nationalRegistryUser.name, + nationalId: nationalRegistryUser.nationalId, + relationships: [ + { + nationalId: nationalRegistryUser.nationalId, + name: nationalRegistryUser.name, + }, + ], + }) + }) - await prDelegationTypeModel.create({ - id: prr.id, - delegationTypeId: dt.id, - personalRepresentativeId: pr.id, - }) - }) + afterAll(() => { + getIndividualRelationships.mockRestore() + }) - describe('when fetched', () => { - let response: request.Response - let body: MergedDelegationDTO[] + it('should return delegations', async () => { + // Arrange + const expectedDelegation = { + fromName: nationalRegistryUser.name, + fromNationalId: nationalRegistryUser.nationalId, + provider: 'fyrirtaekjaskra', + toNationalId: user.nationalId, + type: 'ProcurationHolder', + } as DelegationDTO + // Act + const res = await server.get(`${path}${query}`) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expect(res.body[0]).toEqual( + DelegationDTOMapper.toMergedDelegationDTO(expectedDelegation), + ) + }) - beforeAll(async () => { - response = await server.get(`${path}${query}`) - body = response.body - }) + it('should not return delegations when client does not support procuring holder delegations', async () => { + // Arrange + await clientDelegationTypeModel.destroy({ + where: { + clientId: client.clientId, + delegationType: AuthDelegationType.ProcurationHolder, + }, + }) - it('should have a an OK return status', () => { - expect(response.status).toEqual(200) - }) + // Act + const res = await server.get(`${path}${query}`) - it('should return a single entity', () => { - expect(body.length).toEqual(1) - }) + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) + }) - it('should have the nationalId of the user as the representer', () => { - expect( - body.some((d) => d.toNationalId === user.nationalId), - ).toBeTruthy() - }) + it('should return a procuring holder delegation since the type is included in the delegationTypes filter', async () => { + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.ProcurationHolder}&delegationTypes=${AuthDelegationType.PersonalRepresentative}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expect(res.body[0].types[0]).toEqual( + AuthDelegationType.ProcurationHolder, + ) + }) - it('should have the nationalId of the correct representee', () => { - expect( - body.some( - (d) => d.fromNationalId === nationalRegistryUser.nationalId, - ), - ).toBeTruthy() - }) + it('should not return a procuring holder delegation since the type is not included in the delegationTypes filter', async () => { + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.LegalGuardian}&delegationTypes=${AuthDelegationType.PersonalRepresentative}`, + ) - it('should have the name of the correct representee', () => { - expect( - body.some((d) => d.fromName === nationalRegistryUser.name), - ).toBeTruthy() + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) + }) }) - it('should have the delegation type claim of PersonalRepresentative', () => { - expect( - body.some( - (d) => d.types[0] === AuthDelegationType.PersonalRepresentative, - ), - ).toBeTruthy() - }) - }) + describe('when user is a personal representative with one representee', () => { + let prModel: typeof PersonalRepresentative + let prRightsModel: typeof PersonalRepresentativeRight + let prRightTypeModel: typeof PersonalRepresentativeRightType + let prTypeModel: typeof PersonalRepresentativeType + let delegationTypeModel: typeof DelegationTypeModel + let delegationProviderModel: typeof DelegationProviderModel + let prDelegationTypeModel: typeof PersonalRepresentativeDelegationTypeModel + + beforeAll(async () => { + prTypeModel = app.get( + 'PersonalRepresentativeTypeRepository', + ) + prModel = app.get( + 'PersonalRepresentativeRepository', + ) + prRightTypeModel = app.get< + typeof PersonalRepresentativeRightType + >('PersonalRepresentativeRightTypeRepository') + prRightsModel = app.get( + 'PersonalRepresentativeRightRepository', + ) + delegationTypeModel = app.get( + getModelToken(DelegationTypeModel), + ) + delegationProviderModel = app.get( + getModelToken(DelegationProviderModel), + ) + prDelegationTypeModel = app.get< + typeof PersonalRepresentativeDelegationTypeModel + >(getModelToken(PersonalRepresentativeDelegationTypeModel)) + + const prType = await prTypeModel.create({ + code: 'prTypeCode', + name: 'prTypeName', + description: 'prTypeDescription', + }) + + const pr = await prModel.create({ + nationalIdPersonalRepresentative: user.nationalId, + nationalIdRepresentedPerson: nationalRegistryUser.nationalId, + personalRepresentativeTypeCode: prType.code, + contractId: '1', + externalUserId: '1', + }) + + const dt = await delegationTypeModel.create({ + id: getPersonalRepresentativeDelegationType('prRightType'), + providerId: + AuthDelegationProvider.PersonalRepresentativeRegistry, + name: `Personal Representative: prRightType`, + description: `Personal representative delegation type for right type prRightType`, + }) + + const prRightType = await prRightTypeModel.create({ + code: 'prRightType', + description: 'prRightTypeDescription', + }) + + const prr = await prRightsModel.create({ + rightTypeCode: prRightType.code, + personalRepresentativeId: pr.id, + }) + + await prDelegationTypeModel.create({ + id: prr.id, + delegationTypeId: dt.id, + personalRepresentativeId: pr.id, + }) + }) - it('should not return delegations when client does not support personal representative delegations', async () => { - // Prepare - await clientDelegationTypeModel.destroy({ - where: { - clientId: client.clientId, - delegationType: AuthDelegationType.PersonalRepresentative, - }, - }) + describe('when fetched', () => { + let response: request.Response + let body: MergedDelegationDTO[] + + beforeAll(async () => { + response = await server.get(`${path}${query}`) + body = response.body + }) + + it('should have a an OK return status', () => { + expect(response.status).toEqual(200) + }) + + it('should return a single entity', () => { + expect(body.length).toEqual(1) + }) + + it('should have the nationalId of the user as the representer', () => { + expect( + body.some((d) => d.toNationalId === user.nationalId), + ).toBeTruthy() + }) + + it('should have the nationalId of the correct representee', () => { + expect( + body.some( + (d) => d.fromNationalId === nationalRegistryUser.nationalId, + ), + ).toBeTruthy() + }) + + it('should have the name of the correct representee', () => { + expect( + body.some((d) => d.fromName === nationalRegistryUser.name), + ).toBeTruthy() + }) + + it('should have the delegation type claim of PersonalRepresentative', () => { + expect( + body.some( + (d) => + d.types[0] === AuthDelegationType.PersonalRepresentative, + ), + ).toBeTruthy() + }) + }) - // Act - const res = await server.get(`${path}${query}`) + it('should not return delegations when client does not support personal representative delegations', async () => { + // Prepare + await clientDelegationTypeModel.destroy({ + where: { + clientId: client.clientId, + delegationType: AuthDelegationType.PersonalRepresentative, + }, + }) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) + // Act + const res = await server.get(`${path}${query}`) - it('should return a personal representative delegation since the type is included in the delegationTypes filter', async () => { - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.ProcurationHolder}&delegationTypes=${AuthDelegationType.PersonalRepresentative}`, - ) + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) + }) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(1) - expect(res.body[0].types[0]).toEqual( - AuthDelegationType.PersonalRepresentative, - ) - }) + it('should return a personal representative delegation since the type is included in the delegationTypes filter', async () => { + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.ProcurationHolder}&delegationTypes=${AuthDelegationType.PersonalRepresentative}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expect(res.body[0].types[0]).toEqual( + AuthDelegationType.PersonalRepresentative, + ) + }) - it('should not return a personal representative delegation since the type is not included in the delegationTypes filter', async () => { - // Act - const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.LegalGuardian}&delegationTypes=${AuthDelegationType.ProcurationHolder}`, - ) + it('should not return a personal representative delegation since the type is not included in the delegationTypes filter', async () => { + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.LegalGuardian}&delegationTypes=${AuthDelegationType.ProcurationHolder}`, + ) - // Assert - expect(res.status).toEqual(200) - expect(res.body).toHaveLength(0) - }) + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(0) + }) - afterAll(async () => { - await prRightsModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - await prRightTypeModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - await prModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - await prTypeModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - await prDelegationTypeModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - await delegationTypeModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, - }) - await delegationProviderModel.destroy({ - where: {}, - cascade: true, - truncate: true, - force: true, + afterAll(async () => { + await prRightsModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await prRightTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await prModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await prTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await prDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationProviderModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + }) }) }) }) - }) - }) + }, + ) describe('without auth and permission', () => { it.each` diff --git a/apps/services/auth/public-api/test/mocks/companyRegistryClientService.mock.ts b/apps/services/auth/public-api/test/mocks/companyRegistryClientService.mock.ts new file mode 100644 index 000000000000..fec204a16425 --- /dev/null +++ b/apps/services/auth/public-api/test/mocks/companyRegistryClientService.mock.ts @@ -0,0 +1,8 @@ +import { CompanyRegistryClientService } from '@island.is/clients/rsk/company-registry' + +export const CompanyRegistryClientServiceMock: Partial = + { + getCompany() { + return Promise.resolve(null) + }, + } diff --git a/apps/services/auth/public-api/test/mocks/index.ts b/apps/services/auth/public-api/test/mocks/index.ts index 77ff6e030811..b56b101b6fd9 100644 --- a/apps/services/auth/public-api/test/mocks/index.ts +++ b/apps/services/auth/public-api/test/mocks/index.ts @@ -1,3 +1,4 @@ export * from './einstaklingurApi.mock' export * from './rskProcuringClient.mock' export * from './featureFlagService.mock' +export * from './companyRegistryClientService.mock' diff --git a/apps/services/auth/public-api/test/setup.ts b/apps/services/auth/public-api/test/setup.ts index eb533a696e10..e8eef786b36e 100644 --- a/apps/services/auth/public-api/test/setup.ts +++ b/apps/services/auth/public-api/test/setup.ts @@ -16,6 +16,7 @@ import { IndividualDto, NationalRegistryClientService, } from '@island.is/clients/national-registry-v2' +import { CompanyRegistryClientService } from '@island.is/clients/rsk/company-registry' import { ConfigType } from '@island.is/nest/config' import { FeatureFlagService } from '@island.is/nest/feature-flags' import { @@ -35,6 +36,7 @@ import { import { AppModule } from '../src/app/app.module' import { + CompanyRegistryClientServiceMock, createMockEinstaklingurApi, FeatureFlagServiceMock, RskProcuringClientMock, @@ -156,6 +158,8 @@ export const setupWithAuth = async ({ .useValue(createMockEinstaklingurApi(nationalRegistryUser)) .overrideProvider(RskRelationshipsClient) .useValue(RskProcuringClientMock) + .overrideProvider(CompanyRegistryClientService) + .useValue(CompanyRegistryClientServiceMock) .overrideProvider(DelegationConfig.KEY) .useValue(delegationConfig) .overrideProvider(FeatureFlagService) diff --git a/apps/services/bff/src/app/app.module.ts b/apps/services/bff/src/app/app.module.ts index 923f19224d97..561a14a094a3 100644 --- a/apps/services/bff/src/app/app.module.ts +++ b/apps/services/bff/src/app/app.module.ts @@ -1,5 +1,6 @@ import { ConfigModule } from '@island.is/nest/config' import { Module } from '@nestjs/common' +import { ProblemModule } from '@island.is/nest/problem' import { BffConfig } from './bff.config' import { AuthModule as AppAuthModule } from './modules/auth/auth.module' import { CacheModule } from './modules/cache/cache.module' @@ -18,6 +19,7 @@ import { EnhancedFetchModule } from './modules/enhancedFetch/enhanced-fetch.modu AppAuthModule, ProxyModule, EnhancedFetchModule, + ProblemModule, ], }) export class AppModule {} diff --git a/apps/services/bff/src/app/modules/auth/auth.controller.spec.ts b/apps/services/bff/src/app/modules/auth/auth.controller.spec.ts index 7ccdeda46922..642a1748df73 100644 --- a/apps/services/bff/src/app/modules/auth/auth.controller.spec.ts +++ b/apps/services/bff/src/app/modules/auth/auth.controller.spec.ts @@ -449,8 +449,8 @@ describe('AuthController', () => { expect(res.status).toEqual(HttpStatus.BAD_REQUEST) // Expect error to be expect(res.body).toMatchObject({ - statusCode: 400, - message: 'No param "logout_token" provided!', + status: 400, + detail: 'No param "logout_token" provided!', }) }) diff --git a/apps/services/bff/src/main.ts b/apps/services/bff/src/main.ts index 2e68f2bcbc6e..4a6cf20067ed 100644 --- a/apps/services/bff/src/main.ts +++ b/apps/services/bff/src/main.ts @@ -9,4 +9,5 @@ bootstrap({ port: environment.port, globalPrefix: `${environment.keyPath}/bff`, healthCheck: true, + jsonBodyLimit: '350kb', }) diff --git a/apps/services/endorsements/api/project.json b/apps/services/endorsements/api/project.json index 22c4fe386761..6298812ea242 100644 --- a/apps/services/endorsements/api/project.json +++ b/apps/services/endorsements/api/project.json @@ -33,6 +33,11 @@ "glob": "*", "input": "libs/email-service/src/tools/design", "output": "./email-service-assets" + }, + { + "glob": "*", + "input": "apps/services/endorsements/api/src/assets", + "output": "apps/services/endorsements/api/src/assets" } ] }, diff --git a/apps/services/user-notification/infra/user-notification.ts b/apps/services/user-notification/infra/user-notification.ts index 162a3a82d3d0..d71a994b67c6 100644 --- a/apps/services/user-notification/infra/user-notification.ts +++ b/apps/services/user-notification/infra/user-notification.ts @@ -111,6 +111,7 @@ export const userNotificationServiceSetup = (services: { 'nginx-ingress-internal', 'islandis', 'identity-server-delegation', + 'application-system', ) export const userNotificationWorkerSetup = (services: { diff --git a/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts b/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts index 27baab66a5f4..a1425841a8b8 100644 --- a/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts +++ b/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts @@ -105,6 +105,8 @@ export class NotificationsController { messageId: id, ...flattenedArgs, ...body, + args: {}, // Remove args, since they're in a better format in `flattenedArgs` + queue: { url: this.queue.url, name: this.queue.queueName }, }) return { diff --git a/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.ts b/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.ts index ca638a8aded7..847bf71116af 100644 --- a/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.ts +++ b/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.ts @@ -273,7 +273,7 @@ export class NotificationsWorkerService implements OnApplicationBootstrap { const nationalIdOfOriginalRecipient = message.onBehalfOf?.nationalId ?? profile.nationalId - fullName = await this.getFullName(nationalIdOfOriginalRecipient) + fullName = await this.getName(nationalIdOfOriginalRecipient) } const isEnglish = profile.locale === 'en' @@ -412,7 +412,7 @@ export class NotificationsWorkerService implements OnApplicationBootstrap { let recipientName = '' if (delegations.data.length > 0) { - recipientName = await this.getFullName(message.recipient) + recipientName = await this.getName(message.recipient) } await Promise.all( @@ -441,16 +441,23 @@ export class NotificationsWorkerService implements OnApplicationBootstrap { ) } - private async getFullName(nationalId: string): Promise { - let identity: CompanyExtendedInfo | EinstaklingurDTONafnItar | null + private async getName(nationalId: string): Promise { + try { + let identity: CompanyExtendedInfo | EinstaklingurDTONafnItar | null - if (isCompany(nationalId)) { - identity = await this.companyRegistryService.getCompany(nationalId) - return identity?.name ?? '' - } + if (isCompany(nationalId)) { + identity = await this.companyRegistryService.getCompany(nationalId) + return identity?.name || '' + } - identity = await this.nationalRegistryService.getName(nationalId) - return identity?.birtNafn ?? '' + identity = await this.nationalRegistryService.getName(nationalId) + return identity?.birtNafn || identity?.fulltNafn || '' + } catch (error) { + this.logger.error('Error getting name from national registry', { + error, + }) + return '' + } } /* Private methods */ diff --git a/apps/web/components/Charts/v2/utils/chart.tsx b/apps/web/components/Charts/v2/utils/chart.tsx index e66aa13682af..be0635b9d7f1 100644 --- a/apps/web/components/Charts/v2/utils/chart.tsx +++ b/apps/web/components/Charts/v2/utils/chart.tsx @@ -108,6 +108,9 @@ export const getCartesianGridComponents = ({ type={slice.flipAxis ? 'number' : 'category'} height={customStyleConfig.xAxis?.height ?? DEFAULT_XAXIS_HEIGHT} tick={customStyleConfig.xAxis?.tick ?? undefined} + allowDecimals={ + slice.reduceAndRoundValue === true && slice.flipAxis ? false : true + } />, {slice.yAxisLabel && (