diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..fccf78cc14 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,88 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/alcs-frontend" + schedule: + interval: "daily" + target-branch: "develop" + commit-message: + prefix: "ALCS-000" + allow: + - dependency-type: "direct" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + groups: + npm-security: + applies-to: security-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" + npm-minor-and-patch: + applies-to: version-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" + - package-ecosystem: "npm" + directory: "/portal-frontend" + schedule: + interval: "daily" + target-branch: "develop" + commit-message: + prefix: "ALCS-000" + allow: + - dependency-type: "direct" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + groups: + npm-security: + applies-to: security-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" + npm-minor-and-patch: + applies-to: version-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" + - package-ecosystem: "npm" + directory: "/services" + schedule: + interval: "daily" + target-branch: "develop" + commit-message: + prefix: "ALCS-000" + allow: + - dependency-type: "direct" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + groups: + npm-security: + applies-to: security-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" + npm-minor-and-patch: + applies-to: version-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/_build-image.yml b/.github/workflows/_build-image.yml index 00106984df..a9487103d4 100644 --- a/.github/workflows/_build-image.yml +++ b/.github/workflows/_build-image.yml @@ -33,6 +33,12 @@ jobs: run: | DOCKER_IMAGE=ghcr.io/${{ steps.lowercase_repo_owner.outputs.lowercase }}/${{ inputs.image-name }} TAGS="${DOCKER_IMAGE}:${{ github.sha }},${DOCKER_IMAGE}:latest" + + # Add dev-latest tag for develop branch + if [ "${{ github.ref }}" = "refs/heads/develop" ]; then + TAGS="${TAGS},${DOCKER_IMAGE}:latest-dev" + fi + echo "tags=${TAGS}" >> $GITHUB_OUTPUT echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml new file mode 100644 index 0000000000..a02867e1e4 --- /dev/null +++ b/.github/workflows/auto-merge-dependabot.yml @@ -0,0 +1,38 @@ +name: Auto-merge Dependabot PRs + +on: + pull_request: + branches: + - develop + workflow_run: + workflows: ["CI"] + types: + - completed + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + runs-on: ubuntu-latest + if: | + github.actor == 'dependabot[bot]' && + github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' + steps: + - name: Auto-merge Dependabot PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + run: | + # Get PR number from branch name + PR_NUMBER=$(echo "$HEAD_BRANCH" | grep -o '[0-9]\+' || echo '') + + if [ -n "$PR_NUMBER" ]; then + # Approve PR + gh pr review $PR_NUMBER --approve + + # Enable auto-merge + gh pr merge $PR_NUMBER --auto --merge + fi \ No newline at end of file diff --git a/.github/workflows/backport-to-develop.yml b/.github/workflows/backport-to-develop.yml new file mode 100644 index 0000000000..94c72cd147 --- /dev/null +++ b/.github/workflows/backport-to-develop.yml @@ -0,0 +1,110 @@ +name: Backport to Develop +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + backport: + # Only run if PR was merged (not just closed) and it wasn't from develop + if: | + github.event.pull_request.merged == true && + github.event.pull_request.head.ref != 'develop' + name: Backport to Develop + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + env: + BACKPORT_BRANCH: backport/pr-${{ github.event.pull_request.number }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: develop + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Get First Approver + id: get-approver + run: | + # Get reviews for the original PR + REVIEWS=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews") + + # Extract first APPROVED reviewer's login + FIRST_APPROVER=$(echo "$REVIEWS" | jq -r '.[] | select(.state=="APPROVED") | .user.login' | head -n 1) + + if [ ! -z "$FIRST_APPROVER" ]; then + echo "has_reviewer=true" >> $GITHUB_OUTPUT + echo "reviewer=$FIRST_APPROVER" >> $GITHUB_OUTPUT + else + echo "has_reviewer=false" >> $GITHUB_OUTPUT + fi + + - name: Create backport branch + run: | + # Create a new branch from develop + git checkout -b ${{ env.BACKPORT_BRANCH }} + + # Get the range of commits to cherry-pick + BASE_SHA=$(git merge-base ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}) + + # Cherry pick the range of commits + # Using -m 1 to handle merge commits, and --strategy=recursive --strategy-option=theirs to handle conflicts + if ! git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs ${BASE_SHA}..${{ github.event.pull_request.merge_commit_sha }}; then + if [ -f .git/CHERRY_PICK_HEAD ]; then + # We're in a cherry-pick state + if git diff --cached --quiet && git diff --quiet; then + # No changes in working directory or index - safe to skip + git cherry-pick --skip + else + # There are uncommitted changes - could be conflicts + git cherry-pick --abort + exit 1 + fi + else + # Some other error occurred + exit 1 + fi + fi + + # Push the branch using the token for authentication + git push "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" ${{ env.BACKPORT_BRANCH }} + + - name: Create Pull Request with Reviewer + if: steps.get-approver.outputs.has_reviewer == 'true' + uses: repo-sync/pull-request@v2 + with: + source_branch: ${{ env.BACKPORT_BRANCH }} + destination_branch: "develop" + github_token: ${{ secrets.GITHUB_TOKEN }} + pr_title: "Backport: ${{ github.event.pull_request.title }}" + pr_body: | + Automated backport of changes from main to develop + + Original PR: [#${{ github.event.pull_request.number }} - ${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }}) + Original Author: @${{ github.event.pull_request.user.login }} + pr_label: "backport" + pr_reviewer: ${{ steps.get-approver.outputs.reviewer }} + + - name: Create Pull Request without Reviewer + if: steps.get-approver.outputs.has_reviewer != 'true' + uses: repo-sync/pull-request@v2 + with: + source_branch: ${{ env.BACKPORT_BRANCH }} + destination_branch: "develop" + github_token: ${{ secrets.GITHUB_TOKEN }} + pr_title: "Backport: ${{ github.event.pull_request.title }}" + pr_body: | + Automated backport of changes from main to develop + + Original PR: [#${{ github.event.pull_request.number }} - ${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }}) + Original Author: @${{ github.event.pull_request.user.login }} + pr_label: "backport" \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4c12980754..4d70599e18 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -12,9 +12,15 @@ jobs: with: environment: test secrets: inherit + concurrency: + group: deploy-test + cancel-in-progress: true deploy-prod: needs: deploy-test uses: ./.github/workflows/deploy.yml with: environment: prod - secrets: inherit \ No newline at end of file + secrets: inherit + concurrency: + group: deploy-prod + cancel-in-progress: true \ No newline at end of file diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml new file mode 100644 index 0000000000..4f69dbc994 --- /dev/null +++ b/.github/workflows/trivy-scan.yml @@ -0,0 +1,78 @@ +name: Weekly Trivy DEV Image Scans + +on: + schedule: + # Runs every week at 02:00 Sunday Morning. + - cron: '0 2 * * 0' + workflow_dispatch: + +permissions: + packages: read + security-events: write + +jobs: + image-scan-api: + name: Scan latest-dev API Image + runs-on: ubuntu-latest + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2 + with: + image-ref: 'ghcr.io/bcgov/alcs-api:latest-dev' + format: 'sarif' + output: 'trivy-results.sarif' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + limit-severities-for-sarif: true + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + + image-scan-portal: + name: Scan latest-dev Portal Image + runs-on: ubuntu-latest + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2 + with: + image-ref: 'ghcr.io/bcgov/alcs-portal-frontend:latest-dev' + format: 'sarif' + output: 'trivy-results.sarif' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + limit-severities-for-sarif: true + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + + image-scan-frontend: + name: Scan latest-dev Frontend Image + runs-on: ubuntu-latest + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2 + with: + image-ref: 'ghcr.io/bcgov/alcs-frontend:latest-dev' + format: 'sarif' + output: 'trivy-results.sarif' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + limit-severities-for-sarif: true + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' \ No newline at end of file diff --git a/.github/workflows/zap-scan.yml b/.github/workflows/zap-scan.yml new file mode 100644 index 0000000000..111a9ef650 --- /dev/null +++ b/.github/workflows/zap-scan.yml @@ -0,0 +1,53 @@ +name: Weekly OWASP ZAP Baseline Scan on DEV Site + +on: + schedule: + # Runs every week at 01:00 Sunday Morning. + - cron: '0 1 * * 0' + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + zap-scan-api: + name: OWASP ZAP API Scan + runs-on: ubuntu-latest + steps: + - name: API Scan + uses: zaproxy/action-api-scan@v0.9.0 + with: + target: 'https://alcs-dev-api.apps.silver.devops.gov.bc.ca/docs' + issue_title: OWASP ZAP API Scan Results + artifact_name: zap-api-scan-report + + zap-scan-frontend: + name: OWASP ZAP Frontend Scan + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Frontend Scan + uses: zaproxy/action-baseline@v0.14.0 + with: + target: "https://alcs-dev.apps.silver.devops.gov.bc.ca" + issue_title: OWASP ZAP Frontend Scan Results + rules_file_name: .zap/frontend.tsv + artifact_name: zap-frontend-scan-report + + zap-scan-portal: + name: OWASP ZAP Portal Scan + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Portal Scan + uses: zaproxy/action-baseline@v0.14.0 + with: + target: "https://alcs-dev-portal.apps.silver.devops.gov.bc.ca" + issue_title: OWASP ZAP Portal Scan Results + rules_file_name: .zap/portal.tsv + artifact_name: zap-portal-scan-report \ No newline at end of file diff --git a/.github/workflows/zap_api.yml b/.github/workflows/zap_api.yml deleted file mode 100644 index ac7d0d7925..0000000000 --- a/.github/workflows/zap_api.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: OWASP Zap API Scan -on: workflow_dispatch - -jobs: - zap_scan: - runs-on: ubuntu-latest - name: OWASP Zap API Scan - steps: - - name: ZAP API Scan - uses: zaproxy/action-api-scan@v0.9.0 - with: - target: 'https://alcs-dev-api.apps.silver.devops.gov.bc.ca' - issue_title: OWASP Zap API Results \ No newline at end of file diff --git a/.github/workflows/zap_frontend.yml b/.github/workflows/zap_frontend.yml deleted file mode 100644 index 44b67bdc31..0000000000 --- a/.github/workflows/zap_frontend.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: OWASP Zap Frontend Scan -on: workflow_dispatch - -jobs: - zap_scan: - runs-on: ubuntu-latest - name: OWASP Zap Frontend Scan - steps: - - uses: actions/checkout@v2 - - name: ZAP Scan - uses: zaproxy/action-baseline@v0.14.0 - with: - target: "https://alcs-dev.apps.silver.devops.gov.bc.ca" - issue_title: OWASP Zap Frontend Scan Results - rules_file_name: .zap/frontend.tsv diff --git a/.zap/portal.tsv b/.zap/portal.tsv new file mode 100644 index 0000000000..98c3b974d9 --- /dev/null +++ b/.zap/portal.tsv @@ -0,0 +1,3 @@ +10055 IGNORE (CSP: style-src unsafe-inline) +10015 IGNORE (Incomplete or No Cache-control and Pragma HTTP Header Set) +10110 IGNORE (Dangerous JS Functions) \ No newline at end of file diff --git a/alcs-frontend/Dockerfile b/alcs-frontend/Dockerfile index a188b58063..0e4e9569d5 100644 --- a/alcs-frontend/Dockerfile +++ b/alcs-frontend/Dockerfile @@ -13,7 +13,7 @@ RUN npm ci # Copy the source code to the /app directory COPY . . -ENV NODE_OPTIONS "--max-old-space-size=2048" +ENV NODE_OPTIONS="--max-old-space-size=2048" # Build the application RUN npm run build -- --output-path=dist --output-hashing=all @@ -47,10 +47,10 @@ COPY --from=build /app/dist /usr/share/nginx/html RUN chmod -R go+rwx /usr/share/nginx/html/assets # provide dynamic scp content-src -ENV ENABLED_CONNECT_SRC " 'self' http://localhost:* nrs.objectstore.gov.bc.ca" +ENV ENABLED_CONNECT_SRC=" 'self' http://localhost:* nrs.objectstore.gov.bc.ca" # set to true to enable maintenance mode -ENV MAINTENANCE_MODE "false" +ENV MAINTENANCE_MODE="false" # When the container starts, replace the settings.json with values from environment variables ENTRYPOINT [ "./init.sh" ] diff --git a/alcs-frontend/src/app/features/application/application.component.ts b/alcs-frontend/src/app/features/application/application.component.ts index ac5691070c..b77dee87c9 100644 --- a/alcs-frontend/src/app/features/application/application.component.ts +++ b/alcs-frontend/src/app/features/application/application.component.ts @@ -33,6 +33,8 @@ import { FileTagService } from '../../services/common/file-tag.service'; import { ApplicationDecisionV2Service } from '../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { ApplicationDecisionConditionCardDto } from '../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionConditionCardService } from '../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service'; +import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { ApplicationDecisionConditionFinancialInstrumentService } from '../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service'; export const unsubmittedRoutes = [ { @@ -175,7 +177,13 @@ export const appChildRoutes = [ selector: 'app-application', templateUrl: './application.component.html', styleUrls: ['./application.component.scss'], - providers: [{ provide: FileTagService, useClass: ApplicationTagService }], + providers: [ + { provide: FileTagService, useClass: ApplicationTagService }, + { + provide: DecisionConditionFinancialInstrumentService, + useClass: ApplicationDecisionConditionFinancialInstrumentService, + }, + ], }) export class ApplicationComponent implements OnInit, OnDestroy { destroy = new Subject(); diff --git a/alcs-frontend/src/app/features/application/boundary-amendment/edit-boundary-amendment-dialog/edit-boundary-amendment-dialog.component.html b/alcs-frontend/src/app/features/application/boundary-amendment/edit-boundary-amendment-dialog/edit-boundary-amendment-dialog.component.html index d707d81e66..e0941ba3ca 100644 --- a/alcs-frontend/src/app/features/application/boundary-amendment/edit-boundary-amendment-dialog/edit-boundary-amendment-dialog.component.html +++ b/alcs-frontend/src/app/features/application/boundary-amendment/edit-boundary-amendment-dialog/edit-boundary-amendment-dialog.component.html @@ -1,8 +1,24 @@
-

{{ data ? 'Edit' : 'Create' }} Boundary Amendment

+

{{ data.existingAmendment ? 'Edit' : 'Create' }} Boundary Amendment

+
+ + UUID + + + +
Amendment Type* (null); type = new FormControl('', [Validators.required]); decisionComponents = new FormControl([], [Validators.required]); area = new FormControl(null, [Validators.required]); @@ -22,6 +24,7 @@ export class EditBoundaryAmendmentDialogComponent implements OnInit { selectableComponents: { label: string; value: string }[] = []; form: FormGroup = new FormGroup({ + uuid: this.uuid, type: this.type, decisionComponents: this.decisionComponents, area: this.area, @@ -33,6 +36,7 @@ export class EditBoundaryAmendmentDialogComponent implements OnInit { public matDialogRef: MatDialogRef, private applicationBoundaryAmendmentService: ApplicationBoundaryAmendmentService, private applicationDecisionV2Service: ApplicationDecisionV2Service, + private toastService: ToastService, @Inject(MAT_DIALOG_DATA) public data: { existingAmendment?: ApplicationBoundaryAmendmentDto; @@ -45,6 +49,7 @@ export class EditBoundaryAmendmentDialogComponent implements OnInit { const existingAmendment = this.data.existingAmendment; if (existingAmendment) { this.form.patchValue({ + uuid: existingAmendment.uuid, type: existingAmendment.type, area: existingAmendment.area, year: existingAmendment.year?.toString(), @@ -96,4 +101,11 @@ export class EditBoundaryAmendmentDialogComponent implements OnInit { } this.matDialogRef.close(true); } + + onCopy() { + if (this.uuid.value) { + navigator.clipboard.writeText(this.uuid.value); + this.toastService.showSuccessToast(`${this.uuid.value} copied to clipboard.`); + } + } } diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html index da9ca2c38d..d1c641be32 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html @@ -33,14 +33,9 @@

Create New Condition Card

- - # - {{ element.index }} - - - - Type - {{ element.condition.type.label }} + + Condition + {{ !isOrderNull ? alphaIndex(element.condition.order + 1) + '.' : '' }} {{ element.condition.type.label }} diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts index ef298bfeec..193b9a7842 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts @@ -6,12 +6,12 @@ import { ApplicationDecisionConditionDto, CreateApplicationDecisionConditionCardDto, } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; -import { ApplicationDecisionConditionDto as OriginalApplicationDecisionConditionDto } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionConditionCardService } from '../../../../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service'; import { BOARD_TYPE_CODES, BoardService } from '../../../../../services/board/board.service'; import { BoardDto, BoardStatusDto } from '../../../../../services/board/board.dto'; import { ToastService } from '../../../../../services/toast/toast.service'; import { CardType } from '../../../../../shared/card/card.component'; +import { countToString } from '../../../../../shared/utils/count-to-string'; @Component({ selector: 'app-condition-card-dialog', @@ -19,9 +19,10 @@ import { CardType } from '../../../../../shared/card/card.component'; styleUrl: './condition-card-dialog.component.scss', }) export class ConditionCardDialogComponent implements OnInit { - displayColumns: string[] = ['select', 'index', 'type', 'description']; + displayColumns: string[] = ['select', 'condition', 'description']; conditionBoard: BoardDto | undefined; selectedStatus = ''; + isOrderNull = false; @ViewChild(MatSort) sort!: MatSort; dataSource: MatTableDataSource<{ condition: ApplicationDecisionConditionDto; index: number; selected: boolean }> = @@ -37,6 +38,8 @@ export class ConditionCardDialogComponent implements OnInit { ) {} async ngOnInit() { + const orderIndexes = this.data.conditions.map((c) => c.condition.order); + this.isOrderNull = this.data.conditions.length > 1 && orderIndexes.every((val, i, arr) => val === arr[0] && arr[0] === 0); this.dataSource.data = this.data.conditions.map((item) => ({ condition: item.condition, selected: false, @@ -87,4 +90,8 @@ export class ConditionCardDialogComponent implements OnInit { this.dialogRef.close({ action: 'save', result: false }); } } + + alphaIndex(index: number) { + return countToString(index); + } } diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html index eca365eeda..c7e722f30f 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html @@ -1,7 +1,5 @@
- - -

{{ condition.type.label }}

+

{{ alphaIndex(index) }}. {{ condition.type.label }}

@@ -21,7 +19,7 @@

{{ condition.type.label }}

Security Amount
- {{ condition.securityAmount }} + {{ condition.securityAmount | number }}
@@ -200,7 +198,14 @@

{{ condition.type.label }}

Description
{{ condition.type.label }} >
+ +
+ +
diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss index 31a82f2d74..c630ca0b45 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss @@ -96,3 +96,7 @@ ::ng-deep textarea:focus { outline: none; } + +.condition-instrument { + margin-top: 24px; +} diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts index 3838109676..c3ac914821 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import moment from 'moment'; import { ApplicationDecisionComponentToConditionLotService } from '../../../../../services/application/decision/application-decision-v2/application-decision-component-to-condition-lot/application-decision-component-to-condition-lot.service'; import { ApplicationDecisionConditionService } from '../../../../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service'; @@ -9,6 +9,7 @@ import { UpdateApplicationDecisionConditionDto, DateType, ApplicationDecisionConditionDateDto, + conditionType, } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { DECISION_CONDITION_COMPLETE_LABEL, @@ -27,6 +28,7 @@ import { countToString } from '../../../../../shared/utils/count-to-string'; import { ApplicationDecisionV2Service } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { MatTableDataSource } from '@angular/material/table'; import { MatSort } from '@angular/material/sort'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; type Condition = ApplicationDecisionConditionWithStatus & { componentLabelsStr?: string; @@ -48,6 +50,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { @Input() fileNumber!: string; @Input() index!: number; + @Output() statusChange: EventEmitter = new EventEmitter(); + DateType = DateType; dates: ApplicationDecisionConditionDateDto[] = []; @@ -60,7 +64,6 @@ export class ConditionComponent implements OnInit, AfterViewInit { showAdmFeeField = false; showSecurityAmountField = false; singleDateFormated: string | undefined = undefined; - stringIndex: string = ''; isThreeColumn = true; @@ -72,6 +75,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { subdComponent?: ApplicationDecisionComponentDto; planNumbers: ApplicationDecisionConditionToComponentPlanNumberDto[] = []; + isFinancialSecurity: boolean = false; + displayColumns: string[] = ['index', 'due', 'completed', 'comment', 'action']; @ViewChild(MatSort) sort!: MatSort; @@ -82,10 +87,10 @@ export class ConditionComponent implements OnInit, AfterViewInit { private conditionService: ApplicationDecisionConditionService, private conditionLotService: ApplicationDecisionComponentToConditionLotService, private decisionService: ApplicationDecisionV2Service, + private confirmationDialogService: ConfirmationDialogService, ) {} async ngOnInit() { - this.stringIndex = countToString(this.index); if (this.condition) { this.dates = Array.isArray(this.condition.dates) ? this.condition.dates : []; if (this.condition.type?.dateType === DateType.SINGLE && this.dates.length <= 0) { @@ -114,6 +119,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { this.isRequireSurveyPlan = this.condition.type?.code === 'RSPL'; this.isThreeColumn = this.showAdmFeeField && this.showSecurityAmountField; + this.isFinancialSecurity = this.condition.type?.code === conditionType.FINANCIAL_SECURITY; + this.loadLots(); this.loadPlanNumber(); this.dataSource = new MatTableDataSource( @@ -188,6 +195,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { if (condition) { const update = await this.conditionService.update(condition.uuid, { [field]: value, + order: condition.order, }); const labels = this.condition.componentLabelsStr; @@ -303,23 +311,35 @@ export class ConditionComponent implements OnInit, AfterViewInit { } const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); this.condition.status = conditionNewStatus.status; + this.statusChange.emit(this.condition.status); this.setPillLabel(this.condition.status); } async onDeleteDate(dateUuid: string) { - const result = await this.conditionService.deleteDate(dateUuid); - if (result) { - const index = this.dates.findIndex((date) => date.uuid === dateUuid); - - if (index !== -1) { - this.dates.splice(index, 1); - this.dataSource = new MatTableDataSource( - this.addIndex(this.sortDates(this.dates)), - ); - const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); - this.condition.status = conditionNewStatus.status; - this.setPillLabel(this.condition.status); - } - } + this.confirmationDialogService + .openDialog({ body: 'Are you sure you want to delete this date?' }) + .subscribe(async (confirmed) => { + if (confirmed) { + const result = await this.conditionService.deleteDate(dateUuid); + if (result) { + const index = this.dates.findIndex((date) => date.uuid === dateUuid); + + if (index !== -1) { + this.dates.splice(index, 1); + this.dataSource = new MatTableDataSource( + this.addIndex(this.sortDates(this.dates)), + ); + const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); + this.condition.status = conditionNewStatus.status; + this.statusChange.emit(this.condition.status); + this.setPillLabel(this.condition.status); + } + } + } + }); + } + + alphaIndex(index: number) { + return countToString(index); } } diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html index a5a1ef5554..0a51648f17 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html @@ -17,6 +17,16 @@

View Conditions

+ +
+
Quick Filters:
+ + + {{ label.value }} + + +
+
@@ -34,13 +44,14 @@

View Conditions

-
+
diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss index 121ac38402..c9dcea579b 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss @@ -149,3 +149,46 @@ p { } } } + +.quick-filters { + display: flex; + align-items: center; + gap: 10px; + margin-left: 16px; + margin-top: 12px; +} + +.quick-filters-label { + font-weight: bold; +} + +.quick-filters-button { + background-color: transparent !important; + border: 2px solid transparent; + + &.mat-mdc-chip-selected { + background-color: colors.$grey-dark !important; + + :host::ng-deep & .mdc-evolution-chip__text-label { + color: colors.$white !important; + } + + &:hover { + background-color: colors.$grey !important; + border-color: transparent; + } + } + + &:hover { + border-color: colors.$grey-dark; + + :host::ng-deep & .mdc-evolution-chip__text-label { + color: colors.$black !important; + } + } + + :host::ng-deep & .mdc-evolution-chip__graphic { + visibility: hidden; + width: 0; + } +} diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts index 5599df78d3..184560bff3 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts @@ -22,6 +22,7 @@ import { ApplicationDecisionConditionService } from '../../../../services/applic import { MatDialog } from '@angular/material/dialog'; import { ConditionCardDialogComponent } from './condition-card-dialog/condition-card-dialog.component'; import { ApplicationDecisionConditionCardService } from '../../../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service'; +import { MatChipListboxChange } from '@angular/material/chips'; export type ConditionComponentLabels = { label: string[]; @@ -39,7 +40,7 @@ export type ApplicationDecisionWithConditionComponentLabels = ApplicationDecisio }; export const CONDITION_STATUS = { - COMPLETE: 'COMPLETE', + COMPLETED: 'COMPLETED', ONGOING: 'ONGOING', PENDING: 'PENDING', PASTDUE: 'PASTDUE', @@ -52,6 +53,14 @@ export const CONDITION_STATUS = { styleUrls: ['./conditions.component.scss'], }) export class ConditionsComponent implements OnInit { + conditionLabelsByStatus: Record = { + COMPLETED: 'Complete', + ONGOING: 'Ongoing', + PENDING: 'Pending', + PASTDUE: 'Past Due', + EXPIRED: 'Expired', + }; + $destroy = new Subject(); decisionUuid: string = ''; @@ -68,6 +77,8 @@ export class ConditionsComponent implements OnInit { reconLabel = RECON_TYPE_LABEL; modificationLabel = MODIFICATION_TYPE_LABEL; + conditionFilters: string[] = []; + constructor( private applicationDetailService: ApplicationDetailService, private decisionService: ApplicationDecisionV2Service, @@ -141,23 +152,7 @@ export class ConditionsComponent implements OnInit { decision: ApplicationDecisionWithLinkedResolutionDto, conditions: ApplicationDecisionConditionWithStatus[], ) { - decision.conditions = conditions.sort((a, b) => { - const order = [ - CONDITION_STATUS.ONGOING, - CONDITION_STATUS.COMPLETE, - CONDITION_STATUS.PASTDUE, - CONDITION_STATUS.EXPIRED, - ]; - if (a.status === b.status) { - if (a.type && b.type) { - return a.type?.label.localeCompare(b.type.label); - } else { - return -1; - } - } else { - return order.indexOf(a.status) - order.indexOf(b.status); - } - }); + decision.conditions = conditions.sort((a, b) => a.order - b.order); } private async mapConditions( @@ -216,4 +211,24 @@ export class ConditionsComponent implements OnInit { } }); } + + onConditionFilterChange(change: MatChipListboxChange) { + if (document.activeElement instanceof HTMLElement) { + document.activeElement?.blur(); + } + + this.conditionFilters = change.value; + } + + filterConditions(conditions: ApplicationDecisionConditionWithStatus[]): ApplicationDecisionConditionWithStatus[] { + if (this.conditionFilters.length < 1) { + return conditions; + } + + return conditions.filter((condition) => this.conditionFilters.includes(condition.status)); + } + + onStatusChange(condition: ApplicationDecisionConditionWithStatus, newStatus: string) { + condition.status = newStatus; + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.html index 0ba927a13a..c44f5e0614 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.html @@ -1,8 +1,9 @@
- ALR Area Impacted - (ha) - (m2) +
ALR Area Impacted + (ha) + (m2) +
+ + + +
+
+

Re-order Conditions

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
  + {{ alphaIndex(i + 1) }} + Type + {{ row.type.label }} + Description + {{ row.description }} + + drag_indicator +
+
+
+
+ + +
+
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.scss new file mode 100644 index 0000000000..2ef47ca729 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.scss @@ -0,0 +1,188 @@ +@use '../../../../../../../../styles/colors.scss'; + +.container { + display: flex; + flex-direction: column; + width: 100%; + padding: 12px 8px; + overflow-y: hidden; +} + +.section { + width: 100%; + padding: 16px; +} + +.button-row { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.column-select { + width: 10%; +} + +.column-index { + width: 10%; +} + +.column-type { + width: 30%; +} + +.column-description { + width: 50%; +} + +.conditions-table { + margin-top: 16px; + width: 100%; +} + +.table-container { + max-height: 300px; + overflow-y: auto; + padding: 2px; +} + +.disabled-row { + background-color: #f0f0f0; + opacity: 0.6; + cursor: default; +} + +.menu { + background-color: colors.$white; + box-shadow: 0 4px 4px 0 #00000040; +} + +.menu-item { + display: flex; + flex-direction: column; + margin: 8px 0; + + button { + font-weight: normal !important; + text-align: left; + background-color: colors.$white; + border: none; + cursor: pointer; + } + + button:hover { + background-color: colors.$secondary-color-light; + } + + button.mat-mdc-menu-item { + padding-left: 16px !important; + } +} + +.action-btn.delete { + color: colors.$error-color; +} + +.table-header { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.width-100 { + width: 100%; +} + +.mat-column-action { + width: 8%; + text-align: right; +} + +a { + cursor: pointer; +} + +:host::ng-deep { + .mat-spinner circle { + stroke: #fff; + } +} + +.draggable { + cursor: grab; + + &:hover { + background-color: rgba(colors.$grey-light, 0.5) !important; + } + + &:active { + cursor: grabbing; + } +} + +.selected { + background-color: colors.$grey-light !important; +} + +.drag-cell { + cursor: grab; + + &:active { + cursor: grabbing; + } +} + +.cdk-drag-placeholder, +.cdk-drag-preview { + background-color: colors.$grey-light !important; +} + + +.mat-column-index { + width: 2%; +} + +.mat-column-source { + word-wrap: break-word !important; + white-space: unset !important; + flex: 0 0 10% !important; + width: 10% !important; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.mat-column-type { + word-wrap: break-word !important; + white-space: unset !important; + flex: 0 0 21% !important; + width: 21% !important; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.mat-column-fileName { + word-wrap: break-word !important; + white-space: unset !important; + flex: 0 0 55% !important; + width: 55% !important; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.mat-column-uploadedAt { + width: 18%; +} + +.mat-column-action { + width: 2%; +} + +.mat-column-sorting { + width: 2%; +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts new file mode 100644 index 0000000000..3e851f6b82 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts @@ -0,0 +1,42 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatTableModule } from '@angular/material/table'; +import { MatSortModule } from '@angular/material/sort'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DecisionConditionOrderDialogComponent } from './decision-condition-order-dialog.component'; + +describe('DecisionConditionOrderDialogComponent', () => { + let component: DecisionConditionOrderDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + + await TestBed.configureTestingModule({ + declarations: [DecisionConditionOrderDialogComponent], + imports: [ + MatDialogModule, + BrowserAnimationsModule, + MatTableModule, + MatSortModule, + HttpClientTestingModule, + RouterTestingModule, + ], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: { conditions: [] } }, + { provide: MatDialogRef, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionConditionOrderDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts new file mode 100644 index 0000000000..b2810a0c09 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts @@ -0,0 +1,140 @@ +import { Component, Inject, OnChanges, OnInit, SimpleChanges, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { ApplicationDecisionConditionDto } from '../../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { countToString } from '../../../../../../../shared/utils/count-to-string'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { Overlay, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { MatTableDataSource } from '@angular/material/table'; + +@Component({ + selector: 'app-decision-condition-order-dialog', + templateUrl: './decision-condition-order-dialog.component.html', + styleUrl: './decision-condition-order-dialog.component.scss', +}) +export class DecisionConditionOrderDialogComponent implements OnInit { + displayedColumns = ['index', 'type', 'description', 'actions']; + selectedRecord: string | undefined; + overlayRef: OverlayRef | null = null; + dataSource = new MatTableDataSource([]); + conditionsToOrder: ApplicationDecisionConditionDto[] = []; + + @ViewChild('orderMenu') orderMenu!: TemplateRef; + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: { conditions: ApplicationDecisionConditionDto[]; }, + private dialogRef: MatDialogRef, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + ) {} + + ngOnInit(): void { + const orderIndexes = this.data.conditions.map((c) => c.order); + const isAllZero = orderIndexes.every((val, i, arr) => val === arr[0] && arr[0] === 0); + if (isAllZero) { + let index = 0; + this.data.conditions.forEach((c) => { + c.order = index; + index++; + }); + } + this.conditionsToOrder = this.data.conditions.sort((a,b) => a.order - b.order).map(a => {return {...a}}); + this.dataSource.data = this.conditionsToOrder; + } + + async onRowDropped(event: CdkDragDrop) { + this.moveItem(event.previousIndex, event.currentIndex); + } + + async openMenu($event: MouseEvent, record: ApplicationDecisionConditionDto) { + this.overlayRef?.detach(); + $event.preventDefault(); + this.selectedRecord = record.uuid; + const positionStrategy = this.overlay + .position() + .flexibleConnectedTo({ x: $event.x, y: $event.y }) + .withPositions([ + { + originX: 'end', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top', + }, + ]); + + this.overlayRef = this.overlay.create({ + positionStrategy, + scrollStrategy: this.overlay.scrollStrategies.close(), + }); + + this.overlayRef.attach( + new TemplatePortal(this.orderMenu, this.viewContainerRef, { + $implicit: record, + }), + ); + } + + sendToBottom(record: ApplicationDecisionConditionDto) { + const currentIndex = this.conditionsToOrder.findIndex((item) => item.uuid === record.uuid); + this.conditionsToOrder.sort((a,b) => a.order - b.order).forEach((item) => { + if (item.order > currentIndex) { + item.order--; + } + }); + this.conditionsToOrder[currentIndex].order = this.conditionsToOrder.length; + this.dataSource.data = this.conditionsToOrder.sort((a,b) => a.order - b.order); + this.overlayRef?.detach(); + this.selectedRecord = undefined; + } + + sendToTop(record: ApplicationDecisionConditionDto) { + const currentIndex = this.conditionsToOrder.findIndex((item) => item.uuid === record.uuid); + this.conditionsToOrder.sort((a,b) => a.order - b.order).forEach((item) => { + if (item.order < currentIndex) { + item.order++; + } + }); + this.conditionsToOrder[currentIndex].order = 0; + this.dataSource.data = this.conditionsToOrder.sort((a,b) => a.order - b.order); + this.overlayRef?.detach(); + this.selectedRecord = undefined; + } + + clearMenu() { + this.overlayRef?.detach(); + this.selectedRecord = undefined; + } + + private moveItem(currentIndex: number, targetIndex: number) { + this.conditionsToOrder.sort((a,b) => a.order - b.order).forEach((item) => { + if (currentIndex > targetIndex) { + if (item.order < currentIndex && item.order >= targetIndex) { + item.order++; + } + } else if (item.order > currentIndex) { + if (item.order <= targetIndex) { + item.order--; + } + } + }); + this.conditionsToOrder[currentIndex].order = targetIndex; + this.dataSource.data = this.conditionsToOrder.sort((a,b) => a.order - b.order); + } + + onCancel(): void { + this.dialogRef.close(); + } + + onSave(): void { + const order = this.conditionsToOrder.map((cond, index) => ({ + uuid: cond.uuid, + order: cond.order, + })); + this.dialogRef.close({ payload: order, data: this.conditionsToOrder }); + } + + alphaIndex(index: number) { + return countToString(index); + } +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html index 312752a9b9..8b7be6c59d 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html @@ -1,5 +1,5 @@
-
{{ data.type?.label }}
+
{{ alphaIndex(index + 1) }}. {{ data.type?.label }}
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts index 1ec7ad8821..dbe5184bd9 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts @@ -11,6 +11,9 @@ import { DecisionConditionDateDialogComponent, DueDate, } from './decision-condition-date-dialog/decision-condition-date-dialog.component'; +import { + countToString +} from '../../../../../../../shared/utils/count-to-string'; import moment, { Moment } from 'moment'; import { startWith } from 'rxjs'; @@ -32,6 +35,7 @@ export class DecisionConditionComponent implements OnInit, OnChanges { @Output() remove = new EventEmitter(); @Input() selectableComponents: SelectableComponent[] = []; + @Input() index: number = 0; uuid: string | undefined; @@ -114,6 +118,7 @@ export class DecisionConditionComponent implements OnInit, OnChanges { administrativeFee: this.administrativeFee.value !== null ? parseFloat(this.administrativeFee.value) : undefined, description: this.description.value ?? undefined, componentsToCondition: selectedOptions, + order: this.data.order, }; if (this.showSingleDateField) { @@ -208,7 +213,7 @@ export class DecisionConditionComponent implements OnInit, OnChanges { formatDate(timestamp: number | undefined): string { if (!timestamp) { - return ''; + return 'No Data'; } return moment(timestamp).format('YYYY-MMM-DD'); } @@ -235,4 +240,8 @@ export class DecisionConditionComponent implements OnInit, OnChanges { this.emitChanges(); }); } + + alphaIndex(index: number) { + return countToString(index); + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html index 43da67356e..78005b144e 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html @@ -1,18 +1,6 @@

Conditions

-
- - -
@@ -30,6 +18,22 @@

Conditions

(remove)="onRemoveCondition(index)" [selectableComponents]="selectableComponents" [showDateError]="showDateErrors" + [index]="index" >
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss index a55dff748c..1ee8877aaa 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss @@ -5,6 +5,10 @@ section { margin: 24px 0 56px; } +.order-button { + margin-right: 20px !important; +} + .buttons { margin-top: 28px; } @@ -23,3 +27,8 @@ section { .remove-button { margin-top: 18px !important; } + +.cond-footer { + width: 100%; + text-align: right; +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts index 008299efbd..e415f97e8d 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts @@ -11,24 +11,32 @@ import { ApplicationDecisionV2Service } from '../../../../../../services/applica import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionConditionsComponent } from './decision-conditions.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ApplicationDecisionConditionService } from '../../../../../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service'; describe('DecisionConditionComponent', () => { let component: DecisionConditionsComponent; let fixture: ComponentFixture; let mockDecisionService: ApplicationDecisionV2Service; + let mockConditionService: ApplicationDecisionConditionService; beforeEach(async () => { mockDecisionService = createMock(); + mockConditionService = createMock(); mockDecisionService.$decision = new BehaviorSubject(undefined); mockDecisionService.$decisions = new BehaviorSubject([]); await TestBed.configureTestingModule({ - imports: [MatMenuModule], + imports: [HttpClientTestingModule, MatMenuModule], providers: [ { provide: ApplicationDecisionV2Service, useValue: mockDecisionService, }, + { + provide: ApplicationDecisionConditionService, + useValue: mockConditionService, + }, { provide: ConfirmationDialogService, useValue: {}, diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index 42afea1300..6aeadee27e 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -19,8 +19,11 @@ import { DecisionComponentTypeDto, } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; +import { ApplicationDecisionConditionService } from '../../../../../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service'; import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionConditionComponent } from './decision-condition/decision-condition.component'; +import { DecisionConditionOrderDialogComponent } from './decision-condition-order-dialog/decision-condition-order-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; export type TempApplicationDecisionConditionDto = UpdateApplicationDecisionConditionDto & { tempUuid?: string }; export type SelectableComponent = { uuid?: string; tempId: string; decisionUuid: string; code: string; label: string }; @@ -54,8 +57,10 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy decision: ApplicationDecisionDto | undefined; constructor( + private conditionService: ApplicationDecisionConditionService, private decisionService: ApplicationDecisionV2Service, private confirmationDialogService: ConfirmationDialogService, + protected dialog: MatDialog, ) {} ngOnInit(): void { @@ -98,9 +103,10 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy onAddNewCondition(typeCode: string) { const matchingType = this.activeTypes.find((type) => type.code === typeCode); - this.mappedConditions.unshift({ + this.mappedConditions.push({ type: matchingType, tempUuid: (Math.random() * 10000).toFixed(0), + order: this.mappedConditions.length, }); this.conditionsChange.emit({ conditions: this.mappedConditions, @@ -123,6 +129,11 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy .subscribe((didConfirm) => { if (didConfirm) { this.mappedConditions.splice(index, 1); + this.mappedConditions.forEach((c) => { + if (c.order && c.order > index) { + c.order--; + } + }); this.onChanges(); } }); @@ -213,5 +224,32 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy onValidate() { this.conditionComponents.forEach((component) => component.form.markAllAsTouched()); + this.conditionsChange.emit({ + conditions: this.mappedConditions, + isValid: this.conditionComponents.reduce((isValid, component) => isValid && component.form.valid, true), + }); + } + + openOrderDialog() { + this.dialog + .open(DecisionConditionOrderDialogComponent, { + maxHeight: '80vh', + minHeight: '40vh', + minWidth: '80vh', + data: { + conditions: this.mappedConditions, + }, + }) + .beforeClosed() + .subscribe(async (result) => { + if (result) { + this.conditionService.updateSort(result.payload); + this.mappedConditions = result.data; + this.conditionsChange.emit({ + conditions: this.mappedConditions, + isValid: this.conditionComponents.reduce((isValid, component) => isValid && component.form.valid, true), + }); + } + }); } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index 0a833815b5..86526f94d0 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -262,7 +262,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { (reconsideration) => (existingDecision && existingDecision.reconsiders?.uuid === reconsideration.uuid) || (reconsideration.reviewOutcome?.code === 'PRC' && !reconsideration.resultingDecision) || - (reconsideration.type.code === RECONSIDERATION_TYPE.T_33_1 && !reconsideration.resultingDecision), + (reconsideration.type.code === RECONSIDERATION_TYPE.T_33_1), ) .map((reconsideration, index) => ({ label: `Reconsideration Request #${reconsiderations.length - index} - ${reconsideration.reconsidersDecisions @@ -292,7 +292,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { rescindedComment: existingDecision.rescindedComment, }); - this.conditions = existingDecision.conditions; + this.conditions = existingDecision.conditions.sort((a,b) => a.order - b.order); if (existingDecision.reconsiders) { this.onSelectPostDecision({ diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss index ffe555c27b..6c8500113e 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss @@ -58,7 +58,7 @@ hr { grid-template-rows: auto auto; row-gap: 10px; column-gap: 28px; - margin-bottom: 36px; + margin-bottom: 10px; .title { display: flex; diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts index 6d0d165f1e..f14e25182c 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts @@ -125,7 +125,7 @@ export class DecisionV2Component implements OnInit, OnDestroy { disabledMessage = 'Editing disabled - contact admin'; } decision.conditions.map(async (x) => { - if (x.components) { + if (x.components && x.components.length > 0) { const componentId = x.components[0].uuid; if (componentId) { const conditionStatus = await this.decisionService.getStatus(x.uuid); diff --git a/alcs-frontend/src/app/features/application/decision/decision.module.ts b/alcs-frontend/src/app/features/application/decision/decision.module.ts index 4b3fd69221..44b8739419 100644 --- a/alcs-frontend/src/app/features/application/decision/decision.module.ts +++ b/alcs-frontend/src/app/features/application/decision/decision.module.ts @@ -35,6 +35,8 @@ import { RevertToDraftDialogComponent } from './decision-v2/revert-to-draft-dial import { DecisionComponent } from './decision.component'; import { DecisionConditionDateDialogComponent } from './decision-v2/decision-input/decision-conditions/decision-condition/decision-condition-date-dialog/decision-condition-date-dialog.component'; import { ConditionCardDialogComponent } from './conditions/condition-card-dialog/condition-card-dialog.component'; +import { DecisionConditionOrderDialogComponent } from './decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component'; +import { CdkDrag, CdkDropList } from '@angular/cdk/drag-drop'; export const decisionChildRoutes = [ { @@ -96,7 +98,8 @@ export const decisionChildRoutes = [ ConditionComponent, BasicComponent, ConditionCardDialogComponent, + DecisionConditionOrderDialogComponent, ], - imports: [SharedModule, RouterModule.forChild(decisionChildRoutes), MatTabsModule, MatOptionModule, MatChipsModule], + imports: [SharedModule, RouterModule.forChild(decisionChildRoutes), MatTabsModule, MatOptionModule, MatChipsModule, CdkDropList, CdkDrag], }) export class DecisionModule {} diff --git a/alcs-frontend/src/app/features/application/documents/documents.component.ts b/alcs-frontend/src/app/features/application/documents/documents.component.ts index deb7d5e2f3..108645622e 100644 --- a/alcs-frontend/src/app/features/application/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/application/documents/documents.component.ts @@ -18,6 +18,25 @@ import { DocumentUploadDialogComponent, VisibilityGroup, } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; +import { + DocumentUploadDialogData, + DocumentUploadDialogOptions, +} from '../../../shared/document-upload-dialog/document-upload-dialog.interface'; + +const DOCUMENT_UPLOAD_DIALOG_OPTIONS: DocumentUploadDialogOptions = { + allowedVisibilityFlags: ['A', 'C', 'G', 'P'], + allowsFileEdit: true, + documentTypeOverrides: { + [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { + visibilityGroups: [VisibilityGroup.INTERNAL], + allowsFileEdit: false, + }, + [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { + visibilityGroups: [VisibilityGroup.INTERNAL], + allowsFileEdit: false, + }, + }, +}; @Component({ selector: 'app-documents', @@ -63,37 +82,22 @@ export class DocumentsComponent implements OnInit { } async onUploadFile() { - const submission = await this.applicationSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.applicationParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + fileId: this.fileId, + documentService: this.applicationDocumentService, + parcelService: this.applicationParcelService, + submissionService: this.applicationSubmissionService, + }, + }; this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - documentService: this.applicationDocumentService, - selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), - selectableOwners: submission.owners - .filter((owner) => owner.type.code === 'ORGZ') - .map((owner) => ({ - label: owner.organizationName ?? owner.displayName, - uuid: owner.uuid, - })), - allowedVisibilityFlags: ['A', 'C', 'G', 'P'], - allowsFileEdit: true, - documentTypeOverrides: { - [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - }, - }, + data, }) .afterClosed() .subscribe((isDirty) => { @@ -126,37 +130,24 @@ export class DocumentsComponent implements OnInit { } async onEditFile(element: ApplicationDocumentDto) { - const submission = await this.applicationSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.applicationParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.applicationDocumentService, + parcelService: this.applicationParcelService, + submissionService: this.applicationSubmissionService, + }, + }; this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - existingDocument: element, - documentService: this.applicationDocumentService, - selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), - selectableOwners: submission.owners - .filter((owner) => owner.type.code === 'ORGZ') - .map((owner) => ({ - label: owner.organizationName ?? owner.displayName, - uuid: owner.uuid, - })), - allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, - documentTypeOverrides: { - [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - }, - }, + data, }) .afterClosed() .subscribe((isDirty: boolean) => { diff --git a/alcs-frontend/src/app/features/application/overview/overview.component.html b/alcs-frontend/src/app/features/application/overview/overview.component.html index 99ee613b85..e174490dad 100644 --- a/alcs-frontend/src/app/features/application/overview/overview.component.html +++ b/alcs-frontend/src/app/features/application/overview/overview.component.html @@ -13,6 +13,7 @@

Overview

Proposal Summary
diff --git a/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html b/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html index 4fb9f7cdc3..5928ed46a3 100644 --- a/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html +++ b/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html @@ -38,7 +38,7 @@

Edit Reconsideration

- New Evidence* + New Evidence* Yes No @@ -46,7 +46,7 @@

Edit Reconsideration

- Incorrect or False Info* + Incorrect or False Info* Yes No @@ -54,7 +54,7 @@

Edit Reconsideration

- New Proposal* + New Proposal* Yes No @@ -66,8 +66,8 @@

Edit Reconsideration

-
- Review Outcome * +
+ Review Outcome * Edit Reconsideration
-
+
Does the reconsideration confirm, reverse, or vary the previous decision?**Does the reconsideration confirm, reverse, or vary the previous decision? (null); decisionOutcomeCodeControl = new FormControl(null); reviewDateControl = new FormControl(null); + isNewProposalControl = new FormControl(undefined, [Validators.required]); + isIncorrectFalseInfoControl = new FormControl(undefined, [Validators.required]); + isNewEvidenceControl = new FormControl(undefined, [Validators.required]); + + disable331Fields = false; form: FormGroup = new FormGroup({ submittedDate: new FormControl(undefined, [Validators.required]), @@ -35,9 +40,9 @@ export class EditReconsiderationDialogComponent implements OnInit { reviewDate: this.reviewDateControl, reconsidersDecisions: new FormControl([], [Validators.required]), description: new FormControl('', [Validators.required]), - isNewProposal: new FormControl(undefined, [Validators.required]), - isIncorrectFalseInfo: new FormControl(undefined, [Validators.required]), - isNewEvidence: new FormControl(undefined, [Validators.required]), + isNewProposal: this.isNewProposalControl, + isIncorrectFalseInfo: this.isIncorrectFalseInfoControl, + isNewEvidence: this.isNewEvidenceControl, }); decisions: { uuid: string; resolution: string }[] = []; @@ -70,6 +75,7 @@ export class EditReconsiderationDialogComponent implements OnInit { isIncorrectFalseInfo: parseBooleanToString(data.existingRecon.isIncorrectFalseInfo), isNewEvidence: parseBooleanToString(data.existingRecon.isNewEvidence), }); + this.handleReconType(data.existingRecon.type.code === RECONSIDERATION_TYPE.T_33); } ngOnInit(): void { @@ -93,7 +99,7 @@ export class EditReconsiderationDialogComponent implements OnInit { } = this.form.getRawValue(); const data: UpdateApplicationReconsiderationDto = { submittedDate: formatDateForApi(submittedDate!), - reviewOutcomeCode: reviewOutcomeCode, + reviewOutcomeCode: this.disable331Fields ? 'PRC' : reviewOutcomeCode, decisionOutcomeCode: decisionOutcomeCode, typeCode: type!, reviewDate: reviewDate ? formatDateForApi(reviewDate) : reviewDate, @@ -128,8 +134,10 @@ export class EditReconsiderationDialogComponent implements OnInit { async onTypeReconsiderationChange(reconsiderationType: string) { if (reconsiderationType === RECONSIDERATION_TYPE.T_33_1) { this.reviewOutcomeCodeControl.setValue(null); + this.handleReconType(false); } else { this.reviewOutcomeCodeControl.setValue('PEN'); + this.handleReconType(true); } } @@ -138,4 +146,24 @@ export class EditReconsiderationDialogComponent implements OnInit { this.decisionOutcomeCodeControl.setValue(null); this.reviewDateControl.setValue(null); } + + private handleReconType(enable: boolean) { + if (enable) { + this.isNewEvidenceControl.enable(); + this.isIncorrectFalseInfoControl.enable(); + this.isNewProposalControl.enable(); + this.reviewOutcomeCodeControl.enable(); + this.disable331Fields = false; + } else { + this.isNewEvidenceControl.disable(); + this.isNewEvidenceControl.setValue(null); + this.isIncorrectFalseInfoControl.disable(); + this.isIncorrectFalseInfoControl.setValue(null); + this.isNewProposalControl.disable(); + this.isNewProposalControl.setValue(null); + this.reviewOutcomeCodeControl.disable(); + this.reviewOutcomeCodeControl.setValue(null); + this.disable331Fields = true; + } + } } diff --git a/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html b/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html index d39ea7bc9f..2b8bdd8e77 100644 --- a/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html +++ b/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html @@ -44,17 +44,17 @@
{{ reconsideration.reconsidersDecisionsNumbers.join(', ') }}
-
+
New Evidence
{{ reconsideration.isNewEvidence | booleanToString }}
-
+
Incorrect or False Info
{{ reconsideration.isIncorrectFalseInfo | booleanToString }}
-
+
New Proposal
{{ reconsideration.isNewProposal | booleanToString }}
@@ -91,7 +91,12 @@
-
+
+
Resulting Resolution
+ #{{ reconsideration.resultingDecision.resolutionNumber }}/{{ reconsideration.resultingDecision.resolutionYear }} +
+ +
Does the reconsideration confirm, reverse, or vary {{ reconsideration.reconsidersDecisionsNumbers.join(', ') }}? @@ -103,11 +108,6 @@
{{ reconsideration.decisionOutcome.label }}
- -
-
Resulting Resolution
- #{{ reconsideration.resultingDecision.resolutionNumber }}/{{ reconsideration.resultingDecision.resolutionYear }} -
diff --git a/alcs-frontend/src/app/features/application/proposal/naru/naru.component.html b/alcs-frontend/src/app/features/application/proposal/naru/naru.component.html index 9dc7ecc966..7c89b545c5 100644 --- a/alcs-frontend/src/app/features/application/proposal/naru/naru.component.html +++ b/alcs-frontend/src/app/features/application/proposal/naru/naru.component.html @@ -1,7 +1 @@ -
-
Use End Date
- -
+
diff --git a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html index 5d270e97f9..0633b2f971 100644 --- a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html +++ b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html @@ -14,10 +14,3 @@ (save)="updateApplicationValue('nfuUseSubType', $event)" >
-
-
Use End Date
- -
diff --git a/alcs-frontend/src/app/features/application/proposal/proposal.component.html b/alcs-frontend/src/app/features/application/proposal/proposal.component.html index 4d04777a98..dc252e1cc7 100644 --- a/alcs-frontend/src/app/features/application/proposal/proposal.component.html +++ b/alcs-frontend/src/app/features/application/proposal/proposal.component.html @@ -58,6 +58,7 @@
Parcels
Staff Comments and Observations
diff --git a/alcs-frontend/src/app/features/application/proposal/soil/soil.component.html b/alcs-frontend/src/app/features/application/proposal/soil/soil.component.html index 35d5722387..3f22dc7e92 100644 --- a/alcs-frontend/src/app/features/application/proposal/soil/soil.component.html +++ b/alcs-frontend/src/app/features/application/proposal/soil/soil.component.html @@ -1,29 +1,3 @@ -
-
Use End Date
- -
- -
-
-
Use End Date (Placement of Fill)
- -
- -
-
Use End Date (Removal of Soil)
- -
-
-
Type, origin and quality of fill proposed to be placed.
{{ submission?.soilFillTypeToPlace }} diff --git a/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html index c9fe6bef85..fa71f056f8 100644 --- a/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html @@ -20,7 +20,7 @@

color="accent" mat-flat-button [mat-dialog-close]="isDirty" - [routerLink]="['application', modification.application.fileNumber]" + [routerLink]="['application', modification.application.fileNumber, 'decision']" > View Detail diff --git a/alcs-frontend/src/app/features/board/dialogs/app-modification/create/create-app-modification-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/app-modification/create/create-app-modification-dialog.component.ts index 7453d6bbe5..08ade938ef 100644 --- a/alcs-frontend/src/app/features/board/dialogs/app-modification/create/create-app-modification-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/app-modification/create/create-app-modification-dialog.component.ts @@ -26,7 +26,7 @@ export class CreateAppModificationDialogComponent implements OnInit, OnDestroy { fileNumberControl = new FormControl({ value: '', disabled: true }, [Validators.required]); applicantControl = new FormControl({ value: '', disabled: true }, [Validators.required]); descriptionControl = new FormControl(null, [Validators.required]); - applicationTypeControl = new FormControl(null, [Validators.required]); + applicationTypeControl = new FormControl({ value: null, disabled: true }, [Validators.required]); regionControl = new FormControl({ value: null, disabled: true }, [Validators.required]); submittedDateControl = new FormControl(undefined, [Validators.required]); localGovernmentControl = new FormControl({ value: null, disabled: true }, [Validators.required]); @@ -68,6 +68,7 @@ export class CreateAppModificationDialogComponent implements OnInit, OnDestroy { if (!application.decisionDate) { this.isDecisionDateEmpty = true; } + this.applicationTypeControl.setValue(application.type.code); }); this.applicationService.$applicationTypes.pipe(takeUntil(this.$destroy)).subscribe((types) => { diff --git a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html index 6cf0ff45d8..82a60ec98f 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html @@ -168,7 +168,7 @@

- {{ element.index }}. {{ element.condition.type.label }} + {{ !isOrderNull ? alphaIndex(element.condition.order + 1) + '.' : '' }} {{ element.condition.type.label }} diff --git a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.ts index 4da8884adb..66a09a9a99 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.ts @@ -44,6 +44,7 @@ export class ApplicationDecisionConditionDialogComponent extends CardDialogCompo applicationDecisionConditionCard: ApplicationDecisionConditionCardBoardDto = this.data.decisionConditionCard; isModification: boolean = false; isReconsideration: boolean = false; + isOrderNull = false; @ViewChild(MatSort) sort!: MatSort; dataSource: MatTableDataSource<{ condition: ApplicationDecisionConditionDto; index: number; selected: boolean }> = @@ -95,6 +96,8 @@ export class ApplicationDecisionConditionDialogComponent extends CardDialogCompo true, ); if (decision) { + const orderIndexes = decision.conditions.map((c) => c.order); + this.isOrderNull = decision.conditions.length > 1 && orderIndexes.every((val, i, arr) => val === arr[0] && arr[0] === 0); this.decision = decision; } } @@ -259,4 +262,8 @@ export class ApplicationDecisionConditionDialogComponent extends CardDialogCompo this.applicationDecisionConditionCard = applicationDecisionConditionCard!; this.populateData(); } + + alphaIndex(index: number) { + return countToString(index); + } } diff --git a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts index 382ff334ad..63322c1634 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts @@ -18,6 +18,7 @@ import { UserService } from '../../../../services/user/user.service'; import { ApplicationSubmissionStatusPill } from '../../../../shared/application-submission-status-type-pill/application-submission-status-type-pill.component'; import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { CardDialogComponent } from '../card-dialog/card-dialog.component'; +import { APPLICATION_ROUTER_LINK_BASE } from '../../../../shared/constants'; @Component({ selector: 'app-detail-dialog', @@ -31,7 +32,7 @@ export class ApplicationDialogComponent extends CardDialogComponent implements O application: ApplicationDto = this.data; status?: ApplicationSubmissionStatusPill; - routerLink = `application/`; + routerLink = ''; constructor( @Inject(MAT_DIALOG_DATA) public data: ApplicationDto, @@ -64,7 +65,7 @@ export class ApplicationDialogComponent extends CardDialogComponent implements O async populateApplicationSubmissionStatus(fileNumber: string) { let submissionStatus: ApplicationSubmissionToSubmissionStatusDto | null = null; - this.routerLink = this.routerLink + fileNumber; + this.routerLink = `${APPLICATION_ROUTER_LINK_BASE}/${fileNumber}`; try { submissionStatus = await this.applicationSubmissionStatusService.fetchCurrentStatusByFileNumber( fileNumber, @@ -76,7 +77,7 @@ export class ApplicationDialogComponent extends CardDialogComponent implements O if (submissionStatus) { if (submissionStatus.statusTypeCode === SUBMISSION_STATUS.ALC_DECISION) { - this.routerLink = this.routerLink + '/decision' + this.routerLink = `${APPLICATION_ROUTER_LINK_BASE}/${fileNumber}/decision`; } this.status = { backgroundColor: submissionStatus.status.alcsBackgroundColor, diff --git a/alcs-frontend/src/app/features/board/dialogs/card-dialog/card-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/card-dialog/card-dialog.component.ts index 2c0e31f4fc..7121eec811 100644 --- a/alcs-frontend/src/app/features/board/dialogs/card-dialog/card-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/card-dialog/card-dialog.component.ts @@ -80,7 +80,7 @@ export class CardDialogComponent implements OnInit, OnDestroy { filterAssigneeList(term: string, item: AssigneeDto) { const termLower = term.toLocaleLowerCase(); return ( - item.email.toLocaleLowerCase().indexOf(termLower) > -1 || + (item.email && item.email.toLocaleLowerCase().indexOf(termLower) > -1) || item.prettyName.toLocaleLowerCase().indexOf(termLower) > -1 ); } diff --git a/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html index 5767c928f4..b5901cf82f 100644 --- a/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html @@ -23,7 +23,7 @@

color="accent" mat-flat-button [mat-dialog-close]="isDirty" - [routerLink]="['notice-of-intent', modification.noticeOfIntent.fileNumber]" + [routerLink]="['notice-of-intent', modification.noticeOfIntent.fileNumber, 'decision']" > View Detail diff --git a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts index 1d164524ed..364246e649 100644 --- a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts @@ -20,6 +20,7 @@ import { ApplicationSubmissionStatusPill } from '../../../../shared/application- import { RETROACTIVE_TYPE_LABEL } from '../../../../shared/application-type-pill/application-type-pill.constants'; import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { CardDialogComponent } from '../card-dialog/card-dialog.component'; +import { NOI_ROUTER_LINK_BASE } from '../../../../shared/constants'; @Component({ selector: 'app-notice-of-intent-dialog', @@ -35,7 +36,7 @@ export class NoticeOfIntentDialogComponent extends CardDialogComponent implement noticeOfIntent: NoticeOfIntentDto = this.data; RETROACTIVE_TYPE = RETROACTIVE_TYPE_LABEL; - routerLink = `notice-of-intent/`; + routerLink = ''; constructor( @Inject(MAT_DIALOG_DATA) public data: NoticeOfIntentDto, @@ -78,7 +79,7 @@ export class NoticeOfIntentDialogComponent extends CardDialogComponent implement private async populateSubmissionStatus(fileNumber: string) { let submissionStatus: NoticeOfIntentSubmissionToSubmissionStatusDto | null = null; - this.routerLink = this.routerLink + fileNumber; + this.routerLink = `${NOI_ROUTER_LINK_BASE}/${fileNumber}`; try { submissionStatus = await this.noticeOfIntentSubmissionStatusService.fetchCurrentStatusByFileNumber( fileNumber, @@ -89,7 +90,7 @@ export class NoticeOfIntentDialogComponent extends CardDialogComponent implement } if (submissionStatus) { if (submissionStatus.statusTypeCode === NOI_SUBMISSION_STATUS.ALC_DECISION) { - this.routerLink = this.routerLink + '/decision' + this.routerLink = `${NOI_ROUTER_LINK_BASE}/${fileNumber}/decision`; } this.status = { backgroundColor: submissionStatus.status.alcsBackgroundColor, diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.scss b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.scss index 48315bfa7e..873776a1a4 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.scss +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.scss @@ -62,3 +62,7 @@ mat-dialog-actions { margin-right: 16px; } } + +.field-disabled { + color: colors.$grey; +} diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts index d2808a8277..cb8eb34651 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts @@ -6,6 +6,7 @@ import { Subject, takeUntil } from 'rxjs'; import { ApplicationTypeDto } from '../../../../../services/application/application-code.dto'; import { CreateApplicationReconsiderationDto, + RECONSIDERATION_TYPE, ReconsiderationTypeDto, } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; import { ApplicationReconsiderationService } from '../../../../../services/application/application-reconsideration/application-reconsideration.service'; @@ -29,12 +30,13 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { reconTypes: ReconsiderationTypeDto[] = []; isLoading = false; isDecisionDateEmpty = false; + disable331Fields = false; decisions: { uuid: string; resolution: string }[] = []; fileNumberControl = new FormControl({ value: '', disabled: true }, [Validators.required]); applicantControl = new FormControl({ value: '', disabled: true }, [Validators.required]); - applicationTypeControl = new FormControl(null, [Validators.required]); + applicationTypeControl = new FormControl({ value: null, disabled: true }, [Validators.required]); boardControl = new FormControl(null, [Validators.required]); regionControl = new FormControl({ value: null, disabled: true }, [Validators.required]); submittedDateControl = new FormControl(undefined, [Validators.required]); @@ -87,6 +89,7 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { if (!application.decisionDate) { this.isDecisionDateEmpty = true; } + this.applicationTypeControl.setValue(application.type.code); }); this.applicationService.$applicationTypes.pipe(takeUntil(this.$destroy)).subscribe((types) => { @@ -182,4 +185,29 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { this.$destroy.next(); this.$destroy.complete(); } + + async onTypeReconsiderationChange() { + if (this.reconTypeControl.value === RECONSIDERATION_TYPE.T_33_1) { + this.handleReconType(false); + } else { + this.handleReconType(true); + } + } + + private handleReconType(enable: boolean) { + if (enable) { + this.isNewEvidenceControl.enable(); + this.isIncorrectFalseInfoControl.enable(); + this.isNewProposalControl.enable(); + this.disable331Fields = false; + } else { + this.isNewEvidenceControl.disable(); + this.isNewEvidenceControl.setValue(null); + this.isIncorrectFalseInfoControl.disable(); + this.isIncorrectFalseInfoControl.setValue(null); + this.isNewProposalControl.disable(); + this.isNewProposalControl.setValue(null); + this.disable331Fields = true; + } + } } diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html index d49920ccc9..57c9e410cf 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html @@ -158,25 +158,26 @@

Create Reconsideration

bindValue="code" [clearable]="false" formControlName="reconType" + (change)="onTypeReconsiderationChange()" >

- New Evidence* + New Evidence* Yes No
- Incorrect or False Info* + Incorrect or False Info* Yes No
- New Proposal* + New Proposal* Yes No diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html index 01047723a5..afa6bfb211 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html @@ -20,7 +20,7 @@

color="accent" mat-flat-button [mat-dialog-close]="isDirty" - [routerLink]="['application', recon.application.fileNumber]" + [routerLink]="['application', recon.application.fileNumber, 'decision']" > View Detail diff --git a/alcs-frontend/src/app/features/home/subtask/subtask-table/subtask-table.component.ts b/alcs-frontend/src/app/features/home/subtask/subtask-table/subtask-table.component.ts index 8b7b59ffc0..f16cfe3fc5 100644 --- a/alcs-frontend/src/app/features/home/subtask/subtask-table/subtask-table.component.ts +++ b/alcs-frontend/src/app/features/home/subtask/subtask-table/subtask-table.component.ts @@ -58,7 +58,7 @@ export class SubtaskTableComponent { filterAssigneeList(term: string, item: AssigneeDto) { const termLower = term.toLocaleLowerCase(); return ( - item.email.toLocaleLowerCase().indexOf(termLower) > -1 || + (item.email && item.email.toLocaleLowerCase().indexOf(termLower) > -1) || item.prettyName.toLocaleLowerCase().indexOf(termLower) > -1 ); } diff --git a/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts b/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts index 57a8dde9d7..bf9f10799e 100644 --- a/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts @@ -11,6 +11,7 @@ import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/c import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; import { DocumentUploadDialogComponent } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; +import { DocumentUploadDialogData } from '../../../shared/document-upload-dialog/document-upload-dialog.interface'; @Component({ selector: 'app-documents', @@ -47,16 +48,18 @@ export class DocumentsComponent implements OnInit { } async onUploadFile() { + const data: DocumentUploadDialogData = { + allowsFileEdit: true, + fileId: this.fileId, + documentService: this.inquiryDocumentService, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - documentService: this.inquiryDocumentService, - allowsFileEdit: true, - }, + data, }) .afterClosed() .subscribe((isDirty) => { @@ -89,17 +92,19 @@ export class DocumentsComponent implements OnInit { } onEditFile(element: PlanningReviewDocumentDto) { + const data: DocumentUploadDialogData = { + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.inquiryDocumentService, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - existingDocument: element, - documentService: this.inquiryDocumentService, - allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, - }, + data, }) .afterClosed() .subscribe((isDirty: boolean) => { diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html index 36a0d45ae7..9506e38310 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html @@ -19,7 +19,7 @@

{{ condition.type.label }}

Security Amount
- {{ condition.securityAmount }} + {{ condition.securityAmount | number }}
@@ -145,7 +145,13 @@

{{ condition.type.label }}

Description
{{ condition.type.label }} >
+ +
+ +
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss index 31a82f2d74..c630ca0b45 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss @@ -96,3 +96,7 @@ ::ng-deep textarea:focus { outline: none; } + +.condition-instrument { + margin-top: 24px; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts index 794688a029..bf77fc11ae 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts @@ -1,7 +1,8 @@ -import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import moment from 'moment'; import { NoticeOfIntentDecisionConditionService } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service'; import { + ConditionType, NoticeOfIntentDecisionConditionDateDto, UpdateNoticeOfIntentDecisionConditionDto, } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision.dto'; @@ -19,6 +20,7 @@ import { countToString } from '../../../../../shared/utils/count-to-string'; import { NoticeOfIntentDecisionV2Service } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; type Condition = DecisionConditionWithStatus & { componentLabelsStr?: string; @@ -40,6 +42,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { @Input() fileNumber!: string; @Input() index!: number; + @Output() statusChange: EventEmitter = new EventEmitter(); + DateType = DateType; dates: NoticeOfIntentDecisionConditionDateDto[] = []; @@ -62,6 +66,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { conditionStatus: string = ''; stringIndex: string = ''; + isFinancialSecurity: boolean = false; + displayColumns: string[] = ['index', 'due', 'completed', 'comment', 'action']; @ViewChild(MatSort) sort!: MatSort; @@ -71,6 +77,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { constructor( private conditionService: NoticeOfIntentDecisionConditionService, private decisionService: NoticeOfIntentDecisionV2Service, + private confirmationDialogService: ConfirmationDialogService, ) {} async ngOnInit() { @@ -104,6 +111,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { this.dataSource = new MatTableDataSource( this.addIndex(this.sortDates(this.dates)), ); + + this.isFinancialSecurity = this.condition.type?.code === ConditionType.FINANCIAL_SECURITY; } } @@ -219,6 +228,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); this.condition.status = conditionNewStatus.status; + this.statusChange.emit(this.condition.status); this.setPillLabel(this.condition.status); } else { console.error('Date with specified UUID not found'); @@ -226,20 +236,27 @@ export class ConditionComponent implements OnInit, AfterViewInit { } async onDeleteDate(dateUuid: string) { - const result = await this.conditionService.deleteDate(dateUuid); - if (result) { - const index = this.dates.findIndex((date) => date.uuid === dateUuid); - - if (index !== -1) { - this.dates.splice(index, 1); - this.dataSource = new MatTableDataSource( - this.addIndex(this.sortDates(this.dates)), - ); - - const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); - this.condition.status = conditionNewStatus.status; - this.setPillLabel(this.condition.status); - } - } + this.confirmationDialogService + .openDialog({ body: 'Are you sure you want to delete this date?' }) + .subscribe(async (confirmed) => { + if (confirmed) { + const result = await this.conditionService.deleteDate(dateUuid); + if (result) { + const index = this.dates.findIndex((date) => date.uuid === dateUuid); + + if (index !== -1) { + this.dates.splice(index, 1); + this.dataSource = new MatTableDataSource( + this.addIndex(this.sortDates(this.dates)), + ); + + const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); + this.condition.status = conditionNewStatus.status; + this.statusChange.emit(this.condition.status); + this.setPillLabel(this.condition.status); + } + } + } + }); } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html index 660201cb09..e79f115ef6 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html @@ -17,6 +17,16 @@

View Conditions

+ +
+
Quick Filters:
+ + + {{ label.value }} + + +
+
@@ -29,13 +39,14 @@

View Conditions

-
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss index 121ac38402..a22aaa9b3d 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss @@ -149,3 +149,46 @@ p { } } } + +.quick-filters { + display: flex; + align-items: center; + gap: 10px; + margin-top: 12px; + margin-left: 16px; +} + +.quick-filters-label { + font-weight: bold; +} + +.quick-filters-button { + background-color: transparent !important; + border: 2px solid transparent; + + &.mat-mdc-chip-selected { + background-color: colors.$grey-dark !important; + + :host::ng-deep & .mdc-evolution-chip__text-label { + color: colors.$white !important; + } + + &:hover { + background-color: colors.$grey !important; + border-color: transparent; + } + } + + &:hover { + border-color: colors.$grey-dark; + + :host::ng-deep & .mdc-evolution-chip__text-label { + color: colors.$black !important; + } + } + + :host::ng-deep & .mdc-evolution-chip__graphic { + visibility: hidden; + width: 0; + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts index 505f8a0ec1..43dab3321a 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts @@ -20,6 +20,7 @@ import { NoticeOfIntentDecisionConditionService } from '../../../../services/not import { MatDialog } from '@angular/material/dialog'; import { ConditionCardDialogComponent } from './condition-card-dialog/condition-card-dialog.component'; import { NoticeOfIntentDecisionConditionCardService } from '../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service'; +import { MatChipListboxChange } from '@angular/material/chips'; export type ConditionComponentLabels = { label: string[]; @@ -37,7 +38,7 @@ export type DecisionWithConditionComponentLabels = NoticeOfIntentDecisionWithLin }; export const CONDITION_STATUS = { - COMPLETE: 'complete', + COMPLETED: 'completed', ONGOING: 'ongoing', PENDING: 'pending', PASTDUE: 'pastdue', @@ -50,6 +51,14 @@ export const CONDITION_STATUS = { styleUrls: ['./conditions.component.scss'], }) export class ConditionsComponent implements OnInit { + conditionLabelsByStatus: Record = { + COMPLETED: 'Complete', + ONGOING: 'Ongoing', + PENDING: 'Pending', + PASTDUE: 'Past Due', + EXPIRED: 'Expired', + }; + $destroy = new Subject(); decisionUuid: string = ''; @@ -64,6 +73,8 @@ export class ConditionsComponent implements OnInit { releasedDecisionLabel = RELEASED_DECISION_TYPE_LABEL; modificationLabel = MODIFICATION_TYPE_LABEL; + conditionFilters: string[] = []; + constructor( private noticeOfIntentDetailService: NoticeOfIntentDetailService, private decisionService: NoticeOfIntentDecisionV2Service, @@ -140,7 +151,7 @@ export class ConditionsComponent implements OnInit { decision.conditions = conditions.sort((a, b) => { const order = [ CONDITION_STATUS.ONGOING, - CONDITION_STATUS.COMPLETE, + CONDITION_STATUS.COMPLETED, CONDITION_STATUS.PASTDUE, CONDITION_STATUS.EXPIRED, ]; @@ -212,4 +223,24 @@ export class ConditionsComponent implements OnInit { } }); } + + onConditionFilterChange(change: MatChipListboxChange) { + if (document.activeElement instanceof HTMLElement) { + document.activeElement?.blur(); + } + + this.conditionFilters = change.value; + } + + filterConditions(conditions: DecisionConditionWithStatus[]): DecisionConditionWithStatus[] { + if (this.conditionFilters.length < 1) { + return conditions; + } + + return conditions.filter((condition) => this.conditionFilters.includes(condition.status)); + } + + onStatusChange(condition: DecisionConditionWithStatus, newStatus: string) { + condition.status = newStatus; + } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.html index c3fdf8b008..34f97813be 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.html @@ -1,5 +1,10 @@
-
ALR Area Impacted (ha)
+
+
ALR Area Impacted + (ha) + (m2) +
+
component.form.markAllAsTouched()); + this.conditionsChange.emit({ + conditions: this.mappedConditions, + isValid: this.conditionComponents.reduce((isValid, component) => isValid && component.form.valid, true), + }); } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html index d01d248922..3ffbbc664e 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html @@ -111,8 +111,6 @@
Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }}
- -

Resolution

diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss index ffe555c27b..6c8500113e 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss @@ -58,7 +58,7 @@ hr { grid-template-rows: auto auto; row-gap: 10px; column-gap: 28px; - margin-bottom: 36px; + margin-bottom: 10px; .title { display: flex; diff --git a/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts b/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts index 60fc856461..424e4394d5 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts @@ -23,6 +23,25 @@ import { } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; import { NoticeOfIntentParcelService } from '../../../services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { + DocumentUploadDialogData, + DocumentUploadDialogOptions, +} from '../../../shared/document-upload-dialog/document-upload-dialog.interface'; + +const DOCUMENT_UPLOAD_DIALOG_OPTIONS: DocumentUploadDialogOptions = { + allowedVisibilityFlags: ['A', 'C', 'G', 'P'], + allowsFileEdit: true, + documentTypeOverrides: { + [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { + visibilityGroups: [VisibilityGroup.INTERNAL], + allowsFileEdit: false, + }, + [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { + visibilityGroups: [VisibilityGroup.INTERNAL], + allowsFileEdit: false, + }, + }, +}; @Component({ selector: 'app-noi-documents', @@ -67,37 +86,22 @@ export class NoiDocumentsComponent implements OnInit { } async onUploadFile() { - const submission = await this.noiSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.noiParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + fileId: this.fileId, + documentService: this.noiDocumentService, + parcelService: this.noiParcelService, + submissionService: this.noiSubmissionService, + }, + }; this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - documentService: this.noiDocumentService, - selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), - selectableOwners: submission.owners - .filter((owner) => owner.type.code === 'ORGZ') - .map((owner) => ({ - label: owner.organizationName ?? owner.displayName, - uuid: owner.uuid, - })), - allowedVisibilityFlags: ['A', 'C', 'G', 'P'], - allowsFileEdit: true, - documentTypeOverrides: { - [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - }, - }, + data, }) .afterClosed() .subscribe((isDirty) => { @@ -116,38 +120,24 @@ export class NoiDocumentsComponent implements OnInit { } async onEditFile(element: NoticeOfIntentDocumentDto) { - const submission = await this.noiSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.noiParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.noiDocumentService, + parcelService: this.noiParcelService, + submissionService: this.noiSubmissionService, + }, + }; this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - existingDocument: element, - documentService: this.noiDocumentService, - selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), - selectableOwners: submission.owners - .filter((owner) => owner.type.code === 'ORGZ') - .map((owner) => ({ - label: owner.organizationName ?? owner.displayName, - uuid: owner.uuid, - })), - allowedVisibilityFlags: ['A', 'C', 'G', 'P'], - allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, - documentTypeOverrides: { - [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - }, - }, + data, }) .afterClosed() .subscribe((isDirty: boolean) => { diff --git a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts index 89d3ba4a5c..2bd1e735b8 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts @@ -21,6 +21,8 @@ import { FileTagService } from '../../services/common/file-tag.service'; import { NoticeOfIntentTagService } from '../../services/notice-of-intent/notice-of-intent-tag/notice-of-intent-tag.service'; import { NoticeOfIntentDecisionConditionCardDto } from '../../services/notice-of-intent/decision-v2/notice-of-intent-decision.dto'; import { NoticeOfIntentDecisionConditionCardService } from '../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service'; +import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from '../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service'; export const childRoutes = [ { @@ -96,7 +98,13 @@ const preSubmissionRoutes = [ selector: 'app-notice-of-intent', templateUrl: './notice-of-intent.component.html', styleUrls: ['./notice-of-intent.component.scss'], - providers: [{ provide: FileTagService, useClass: NoticeOfIntentTagService }], + providers: [ + { provide: FileTagService, useClass: NoticeOfIntentTagService }, + { + provide: DecisionConditionFinancialInstrumentService, + useClass: NoticeOfIntentDecisionConditionFinancialInstrumentService, + }, + ], }) export class NoticeOfIntentComponent implements OnInit, OnDestroy { destroy = new Subject(); diff --git a/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.html b/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.html index 8e8960fa3e..cfc537b6db 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.html @@ -13,6 +13,7 @@

Overview

Proposal Summary
diff --git a/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html b/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html index 04c06fd85b..bc8e4a0bd3 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html @@ -49,6 +49,7 @@
Parcels
Staff Comments and Observations
diff --git a/alcs-frontend/src/app/features/notice-of-intent/proposal/soil/soil.component.html b/alcs-frontend/src/app/features/notice-of-intent/proposal/soil/soil.component.html index ea25ce1ee3..188449b008 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/proposal/soil/soil.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/proposal/soil/soil.component.html @@ -1,29 +1,3 @@ -
-
Use End Date
- -
- -
-
-
Use End Date (Placement of Fill)
- -
- -
-
Use End Date (Removal of Soil)
- -
-
-
Type, origin and quality of fill proposed to be placed.
{{ submission?.soilFillTypeToPlace }} diff --git a/alcs-frontend/src/app/features/notification/documents/documents.component.ts b/alcs-frontend/src/app/features/notification/documents/documents.component.ts index 304785905a..db64f5ba9b 100644 --- a/alcs-frontend/src/app/features/notification/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/notification/documents/documents.component.ts @@ -12,6 +12,15 @@ import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/c import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; import { DocumentUploadDialogComponent } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; +import { + DocumentUploadDialogData, + DocumentUploadDialogOptions, +} from '../../../shared/document-upload-dialog/document-upload-dialog.interface'; + +const DOCUMENT_UPLOAD_DIALOG_OPTIONS: DocumentUploadDialogOptions = { + allowedVisibilityFlags: ['A', 'G', 'P'], + allowsFileEdit: true, +}; @Component({ selector: 'app-notification-documents', @@ -48,17 +57,20 @@ export class NotificationDocumentsComponent implements OnInit { } async onUploadFile() { + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + fileId: this.fileId, + documentService: this.notificationDocumentService, + }, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - documentService: this.notificationDocumentService, - allowedVisibilityFlags: ['A', 'G', 'P'], - allowsFileEdit: true, - }, + data, }) .afterClosed() .subscribe((isDirty) => { @@ -91,18 +103,22 @@ export class NotificationDocumentsComponent implements OnInit { } onEditFile(element: NoticeOfIntentDocumentDto) { + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.notificationDocumentService, + }, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - existingDocument: element, - documentService: this.notificationDocumentService, - allowedVisibilityFlags: ['A', 'G', 'P'], - allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, - }, + data, }) .afterClosed() .subscribe((isDirty: boolean) => { diff --git a/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts b/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts index f666bed563..cb8e66818c 100644 --- a/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts @@ -10,6 +10,15 @@ import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/c import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; import { DocumentUploadDialogComponent } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; +import { + DocumentUploadDialogData, + DocumentUploadDialogOptions, +} from '../../../shared/document-upload-dialog/document-upload-dialog.interface'; + +const DOCUMENT_UPLOAD_DIALOG_OPTIONS: DocumentUploadDialogOptions = { + allowedVisibilityFlags: ['C'], + allowsFileEdit: true, +}; @Component({ selector: 'app-documents', @@ -48,17 +57,20 @@ export class DocumentsComponent implements OnInit { } async onUploadFile() { + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + fileId: this.fileId, + documentService: this.planningReviewDocumentService, + }, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - documentService: this.planningReviewDocumentService, - allowedVisibilityFlags: ['C'], - allowsFileEdit: true, - }, + data, }) .afterClosed() .subscribe((isDirty) => { @@ -91,18 +103,22 @@ export class DocumentsComponent implements OnInit { } onEditFile(element: PlanningReviewDocumentDto) { + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.planningReviewDocumentService, + }, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - existingDocument: element, - documentService: this.planningReviewDocumentService, - allowsFileEdit: true, - allowedVisibilityFlags: ['C'], - }, + data, }) .afterClosed() .subscribe((isDirty: boolean) => { diff --git a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts index dd015bc2c1..4a033e7926 100644 --- a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts +++ b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts @@ -9,6 +9,7 @@ import { TreeNode, } from '../../../services/search/file-type/file-type-data-source.service'; import { PortalStatusDataSourceService } from '../../../services/search/portal-status/portal-status-data-source.service'; +import { DecisionOutcomeDataSourceService } from '../../../services/search/decision-outcome/decision-outcome-data-source.service'; @Component({ selector: 'app-file-type-filter-drop-down[fileTypeData]', @@ -35,7 +36,7 @@ export class FileTypeFilterDropDownComponent implements OnInit { componentTypeControl = new FormControl(undefined); @Output() fileTypeChange = new EventEmitter(); - @Input() fileTypeData!: FileTypeDataSourceService | PortalStatusDataSourceService; + @Input() fileTypeData!: FileTypeDataSourceService | PortalStatusDataSourceService | DecisionOutcomeDataSourceService; @Input() label!: string; @Input() tooltip = ''; @Input() preExpanded: string[] = []; diff --git a/alcs-frontend/src/app/features/search/search.component.html b/alcs-frontend/src/app/features/search/search.component.html index 935d1130ae..ea275bbf4f 100644 --- a/alcs-frontend/src/app/features/search/search.component.html +++ b/alcs-frontend/src/app/features/search/search.component.html @@ -81,6 +81,33 @@

File Details

+
+
+ +
+ +
+ + +
{{ item.label }}
+
+
+
+
(undefined); portalStatusControl = new FormControl([]); + decisionOutcomeControl = new FormControl([]); + decisionMakerControl = new FormControl(undefined); componentTypeControl = new FormControl(undefined); pidControl = new FormControl(undefined); nameControl = new FormControl(undefined); @@ -109,6 +113,8 @@ export class SearchComponent implements OnInit, OnDestroy { resolutionYear: new FormControl(undefined), legacyId: new FormControl(undefined), portalStatus: this.portalStatusControl, + decisionOutcome: this.decisionOutcomeControl, + decisionMaker: this.decisionMakerControl, componentType: this.componentTypeControl, government: this.localGovernmentControl, region: new FormControl(undefined), @@ -127,6 +133,7 @@ export class SearchComponent implements OnInit, OnDestroy { allTags: TagDto[] = []; tagCategories: TagCategoryDto[] = []; applicationStatuses: ApplicationStatusDto[] = []; + decisionMakers: DecisionMakerDto[] = []; allStatuses: (ApplicationStatusDto | NoticeOfIntentStatusDto | NotificationSubmissionStatusDto)[] = []; formEmpty = true; @@ -151,6 +158,7 @@ export class SearchComponent implements OnInit, OnDestroy { private authService: AuthenticationService, public fileTypeService: FileTypeDataSourceService, public portalStatusDataService: PortalStatusDataSourceService, + public decisionOutcomeDataService: DecisionOutcomeDataSourceService, public tagCategoryService: TagCategoryService, public tagService: TagService, ) { @@ -164,10 +172,11 @@ export class SearchComponent implements OnInit, OnDestroy { this.applicationService.$applicationRegions .pipe(takeUntil(this.$destroy)) - .pipe(combineLatestWith(this.applicationService.$applicationStatuses, this.activatedRoute.queryParamMap)) - .subscribe(([regions, statuses, queryParamMap]) => { + .pipe(combineLatestWith(this.applicationService.$applicationStatuses, this.applicationService.$decisionMakers, this.activatedRoute.queryParamMap)) + .subscribe(([regions, statuses, decisionMakers, queryParamMap]) => { this.regions = regions; this.applicationStatuses = statuses; + this.decisionMakers = decisionMakers; this.populateAllStatuses(); const searchText = queryParamMap.get('searchText'); @@ -301,6 +310,8 @@ export class SearchComponent implements OnInit, OnDestroy { const resolutionNumberString = this.formatStringSearchParam(this.searchForm.controls.resolutionNumber.value); let fileTypes: string[]; let portalStatusCodes; + let decisionMakers; + let decisionOutcomes: string[]; if (this.searchForm.controls.componentType.value === null) { fileTypes = this.isCommissioner ? this.fileTypeService.getCommissionerListData() : []; @@ -308,6 +319,18 @@ export class SearchComponent implements OnInit, OnDestroy { fileTypes = this.searchForm.controls.componentType.value!; } + if (this.searchForm.controls.decisionOutcome.value === null) { + decisionOutcomes = []; + } else { + decisionOutcomes = this.searchForm.controls.decisionOutcome.value!; + } + + if (this.searchForm.controls.decisionOutcome.value === null) { + decisionMakers = []; + } else { + decisionMakers = this.searchForm.controls.decisionMaker.value!; + } + if (this.searchForm.controls.portalStatus.value?.length === 0) { portalStatusCodes = this.isCommissioner ? this.portalStatusDataService.getCommissionerListData() : []; } else { @@ -347,6 +370,8 @@ export class SearchComponent implements OnInit, OnDestroy { fileTypes: fileTypes, tagCategoryId: this.searchForm.controls.tagCategory.value ?? undefined, tagIds: this.tags.map((t) => t.uuid), + decisionOutcomes: decisionOutcomes, + decisionMaker: this.searchForm.controls.decisionMaker.value ?? undefined, }; } @@ -444,6 +469,10 @@ export class SearchComponent implements OnInit, OnDestroy { this.portalStatusControl.setValue(statusCodes); } + onDecisionOutcomeChange(decisionOutcomes: string[]) { + this.decisionOutcomeControl.setValue(decisionOutcomes); + } + onClick(): void { this.clicked = true; if (!this.firstClicked) { diff --git a/alcs-frontend/src/app/services/application/application-code.dto.ts b/alcs-frontend/src/app/services/application/application-code.dto.ts index 25d1e734a3..94e2a338d2 100644 --- a/alcs-frontend/src/app/services/application/application-code.dto.ts +++ b/alcs-frontend/src/app/services/application/application-code.dto.ts @@ -1,6 +1,7 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; import { ReconsiderationTypeDto } from './application-reconsideration/application-reconsideration.dto'; import { ApplicationStatusDto } from './application-submission-status/application-submission-status.dto'; +import { DecisionMakerDto } from './decision/application-decision-v2/application-decision.dto'; export interface CardStatusDto extends BaseCodeDto {} export interface ApplicationRegionDto extends BaseCodeDto {} @@ -18,4 +19,5 @@ export interface ApplicationMasterCodesDto { region: ApplicationRegionDto[]; reconsiderationType: ReconsiderationTypeDto[]; applicationStatusType: ApplicationStatusDto[]; + decisionMaker: DecisionMakerDto[]; } diff --git a/alcs-frontend/src/app/services/application/application.service.ts b/alcs-frontend/src/app/services/application/application.service.ts index 8b927b0ba7..f0f0623da3 100644 --- a/alcs-frontend/src/app/services/application/application.service.ts +++ b/alcs-frontend/src/app/services/application/application.service.ts @@ -11,6 +11,7 @@ import { } from './application-code.dto'; import { ApplicationStatusDto } from './application-submission-status/application-submission-status.dto'; import { ApplicationDto, CreateApplicationDto, UpdateApplicationDto } from './application.dto'; +import { DecisionMakerDto } from './decision/application-decision-v2/application-decision.dto'; @Injectable({ providedIn: 'root', @@ -20,11 +21,13 @@ export class ApplicationService { $applicationTypes = new BehaviorSubject([]); $applicationRegions = new BehaviorSubject([]); $applicationStatuses = new BehaviorSubject([]); + $decisionMakers = new BehaviorSubject([]); private baseUrl = `${environment.apiUrl}/application`; private statuses: CardStatusDto[] = []; private types: ApplicationTypeDto[] = []; private regions: ApplicationRegionDto[] = []; private applicationStatuses: ApplicationStatusDto[] = []; + private decisionMakers: DecisionMakerDto[] = []; private isInitialized = false; constructor( @@ -119,5 +122,8 @@ export class ApplicationService { this.applicationStatuses = codes.applicationStatusType; this.$applicationStatuses.next(this.applicationStatuses); + + this.decisionMakers = codes.decisionMaker; + this.$decisionMakers.next(this.decisionMakers); } } diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts new file mode 100644 index 0000000000..5ad6a66d18 --- /dev/null +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts @@ -0,0 +1,8 @@ +import { + CreateUpdateDecisionConditionFinancialInstrumentDto, + DecisionConditionFinancialInstrumentDto, +} from '../../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; + +export interface ApplicationDecisionConditionFinancialInstrumentDto extends DecisionConditionFinancialInstrumentDto {} +export interface CreateUpdateApplicationDecisionConditionFinancialInstrumentDto + extends CreateUpdateDecisionConditionFinancialInstrumentDto {} diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts new file mode 100644 index 0000000000..f1dc449b1d --- /dev/null +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts @@ -0,0 +1,335 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ApplicationDecisionConditionFinancialInstrumentService } from './application-decision-condition-financial-instrument.service'; +import { + ApplicationDecisionConditionFinancialInstrumentDto, + CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, +} from './application-decision-condition-financial-instrument.dto'; +import { + HeldBy, + InstrumentStatus, + InstrumentType, +} from '../../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; +import { environment } from '../../../../../../../environments/environment'; + +describe('ApplicationDecisionConditionFinancialInstrumentService', () => { + let service: ApplicationDecisionConditionFinancialInstrumentService; + let mockHttpClient: DeepMocked; + const conditionId = '1'; + const instrumentId = '1'; + let expectedUrl: string; + + beforeEach(() => { + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(ApplicationDecisionConditionFinancialInstrumentService); + expectedUrl = `${environment.apiUrl}/v2/application-decision-condition/${conditionId}/financial-instruments`; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make an http get for getAll', async () => { + const mockResponse: ApplicationDecisionConditionFinancialInstrumentDto[] = [ + { + uuid: '1', + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }, + ]; + + mockHttpClient.get.mockReturnValue(of(mockResponse)); + + const result = await service.getAll(conditionId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if getAll fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.getAll(conditionId)).rejects.toThrow('Failed to retrieve the financial instruments'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl); + }); + + it('should make an http get for get', async () => { + const mockResponse: ApplicationDecisionConditionFinancialInstrumentDto = { + uuid: '1', + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.get.mockReturnValue(of(mockResponse)); + + const result = await service.get(conditionId, instrumentId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if get fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should make an http post for create', async () => { + const mockRequest: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + const mockResponse: ApplicationDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.post.mockReturnValue(of(mockResponse)); + + const result = await service.create(conditionId, mockRequest); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, mockRequest); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if create fails', async () => { + const mockRequest: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.post.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.create(conditionId, mockRequest)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, mockRequest); + }); + + it('should make an http patch for update', async () => { + const mockRequest: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + const mockResponse: ApplicationDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.patch.mockReturnValue(of(mockResponse)); + + const result = await service.update(conditionId, instrumentId, mockRequest); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, mockRequest); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if update fails', async () => { + const mockRequest: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.patch.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.update(conditionId, instrumentId, mockRequest)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, mockRequest); + }); + + it('should make an http delete for delete', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.delete(conditionId, instrumentId); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should show an error if delete fails', async () => { + mockHttpClient.delete.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.delete(conditionId, instrumentId)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should remove statusDate and explanation when status is RECEIVED during update', async () => { + const mockRequest: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + statusDate: 1672531200000, + explanation: 'test', + instrumentNumber: '123', + }; + + const mockResponse: ApplicationDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.patch.mockReturnValue(of(mockResponse)); + + await service.update(conditionId, instrumentId, mockRequest); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }); + }); + + it('should throw Condition/financial instrument not found', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 404, error: { message: 'Condition/financial instrument not found' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition/financial instrument not found'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should throw Condition is not of type Financial Security', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 400, error: { message: 'Condition is not of type Financial Security' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition is not of type Financial Security'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should throw Condition type Financial Security not found', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 500, error: { message: 'Condition type Financial Security not found' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition type Financial Security not found'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); +}); diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts new file mode 100644 index 0000000000..00730ae3dd --- /dev/null +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; +import { + ApplicationDecisionConditionFinancialInstrumentDto, + CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, +} from './application-decision-condition-financial-instrument.dto'; +import { DecisionConditionFinancialInstrumentService } from '../../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { environment } from '../../../../../../../environments/environment'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; +import { InstrumentStatus } from '../../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplicationDecisionConditionFinancialInstrumentService extends DecisionConditionFinancialInstrumentService { + private baseUrl = `${environment.apiUrl}/v2/application-decision-condition`; + private financialInstrumentUrl = 'financial-instruments'; + + constructor(http: HttpClient) { + super(http); + } + async getAll(conditionUuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}`; + + try { + return await firstValueFrom(this.http.get(url)); + } catch (e) { + this.handleError(e); + } + } + + async get(conditionUuid: string, uuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + try { + return await firstValueFrom(this.http.get(url)); + } catch (e) { + this.handleError(e); + } + } + + async create( + conditionUuid: string, + dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}`; + + try { + return await firstValueFrom(this.http.post(url, dto)); + } catch (e) { + this.handleError(e); + } + } + + async update( + conditionUuid: string, + uuid: string, + dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + if (dto.status === InstrumentStatus.RECEIVED) { + if (dto.statusDate) { + delete dto.statusDate; + } + if (dto.explanation) { + delete dto.explanation; + } + } + + try { + return await firstValueFrom(this.http.patch(url, dto)); + } catch (e) { + this.handleError(e); + } + } + + async delete(conditionUuid: string, uuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + try { + return await firstValueFrom(this.http.delete(url)); + } catch (e) { + this.handleError(e); + } + } + + private handleError(e: any): never { + console.error(e); + let message; + if (e instanceof HttpErrorResponse) { + if (e.status === 404) { + message = 'Condition/financial instrument not found'; + } else if (e.status === 400) { + message = 'Condition is not of type Financial Security'; + } else { + if (e.error.message === 'Condition type Financial Security not found') { + message = 'Condition type Financial Security not found'; + } else { + message = 'Failed to retrieve the financial instruments'; + } + } + } else { + message = 'Failed to perform the operation'; + } + throw new Error(message); + } +} diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.ts index c0a8329c7d..2eb83321dd 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.ts @@ -131,4 +131,12 @@ export class ApplicationDecisionConditionService { return; } } + + async updateSort(sortOrder: { uuid: string; order: number }[]) { + try { + await firstValueFrom(this.http.post(`${this.url}/sort`, sortOrder)); + } catch (e) { + this.toastService.showErrorToast(`Failed to save conditions order`); + } + } } diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts index 504b541f43..18d367c664 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts @@ -275,6 +275,10 @@ export enum DateType { MULTIPLE = 'Multiple', } +export enum conditionType { + FINANCIAL_SECURITY = 'BOND', +} + export interface ApplicationDecisionConditionTypeDto extends BaseCodeDto { isActive: boolean; isComponentToConditionChecked?: boolean | null; @@ -305,6 +309,7 @@ export interface ApplicationDecisionConditionDto { conditionCard?: ApplicationDecisionConditionCardDto | null; status?: string | null; decision: ApplicationDecisionDto | null; + order: number; } export interface UpdateApplicationDecisionConditionDto { @@ -316,6 +321,7 @@ export interface UpdateApplicationDecisionConditionDto { description?: string | null; type?: ApplicationDecisionConditionTypeDto | null; dates?: ApplicationDecisionConditionDateDto[]; + order?: number; } export interface ComponentToCondition { diff --git a/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto.ts b/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto.ts new file mode 100644 index 0000000000..e023bde6b0 --- /dev/null +++ b/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto.ts @@ -0,0 +1,53 @@ +export enum InstrumentType { + BANK_DRAFT = 'Bank Draft', + CERTIFIED_CHEQUE = 'Certified Cheque', + EFT = 'EFT', + IRREVOCABLE_LETTER_OF_CREDIT = 'Irrevocable Letter of Credit', + OTHER = 'Other', + SAFEKEEPING_AGREEMENT = 'Safekeeping Agreement', +} + +export enum HeldBy { + ALC = 'ALC', + MINISTRY = 'Ministry', +} + +export enum InstrumentStatus { + RECEIVED = 'Received', + RELEASED = 'Released', + CASHED = 'Cashed', + REPLACED = 'Replaced', +} + +export interface DecisionConditionFinancialInstrumentDto { + uuid: string; + securityHolderPayee: string; + type: InstrumentType; + issueDate: number; + expiryDate?: number | null; + amount: number; + bank: string; + instrumentNumber?: string | null; + heldBy: HeldBy; + receivedDate: number; + notes?: string | null; + status: InstrumentStatus; + statusDate?: number | null; + explanation?: string | null; +} + +export interface CreateUpdateDecisionConditionFinancialInstrumentDto { + securityHolderPayee: string; + type: InstrumentType; + issueDate: number; + expiryDate?: number | null; + amount: number; + bank: string; + instrumentNumber?: string | null; + heldBy: HeldBy; + receivedDate: number; + notes?: string | null; + status: InstrumentStatus; + statusDate?: number | null; + explanation?: string | null; +} diff --git a/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service.ts b/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service.ts new file mode 100644 index 0000000000..13938b951f --- /dev/null +++ b/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service.ts @@ -0,0 +1,30 @@ +import { HttpClient } from '@angular/common/http'; +import { + CreateUpdateDecisionConditionFinancialInstrumentDto, + DecisionConditionFinancialInstrumentDto, +} from './decision-condition-financial-instrument.dto'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export abstract class DecisionConditionFinancialInstrumentService { + constructor(protected http: HttpClient) {} + + abstract getAll(conditionUuid: string): Promise; + + abstract get(conditionUuid: string, uuid: string): Promise; + + abstract create( + conditionUuid: string, + dto: CreateUpdateDecisionConditionFinancialInstrumentDto, + ): Promise; + + abstract update( + conditionUuid: string, + uuid: string, + dto: CreateUpdateDecisionConditionFinancialInstrumentDto, + ): Promise; + + abstract delete(conditionUuid: string, uuid: string): Promise; +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts new file mode 100644 index 0000000000..bcbcecdfc5 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts @@ -0,0 +1,9 @@ +import { + CreateUpdateDecisionConditionFinancialInstrumentDto, + DecisionConditionFinancialInstrumentDto, +} from '../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; + +export interface NoticeOfIntentDecisionConditionFinancialInstrumentDto + extends DecisionConditionFinancialInstrumentDto {} +export interface CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto + extends CreateUpdateDecisionConditionFinancialInstrumentDto {} diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts new file mode 100644 index 0000000000..a746b4001f --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts @@ -0,0 +1,335 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from './notice-of-intent-decision-condition-financial-instrument.service'; +import { environment } from '../../../../../../environments/environment'; +import { + CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, +} from './notice-of-intent-decision-condition-financial-instrument.dto'; +import { + HeldBy, + InstrumentStatus, + InstrumentType, +} from '../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; + +describe('NoticeOfIntentDecisionConditionFinancialInstrumentService', () => { + let service: NoticeOfIntentDecisionConditionFinancialInstrumentService; + let mockHttpClient: DeepMocked; + const conditionId = '1'; + const instrumentId = '1'; + let expectedUrl: string; + + beforeEach(() => { + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentDecisionConditionFinancialInstrumentService); + expectedUrl = `${environment.apiUrl}/notice-of-intent-decision-condition/${conditionId}/financial-instruments`; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make an http get for getAll', async () => { + const mockResponse: NoticeOfIntentDecisionConditionFinancialInstrumentDto[] = [ + { + uuid: '1', + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }, + ]; + + mockHttpClient.get.mockReturnValue(of(mockResponse)); + + const result = await service.getAll(conditionId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if getAll fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.getAll(conditionId)).rejects.toThrow('Failed to retrieve the financial instruments'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl); + }); + + it('should make an http get for get', async () => { + const mockResponse: NoticeOfIntentDecisionConditionFinancialInstrumentDto = { + uuid: '1', + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.get.mockReturnValue(of(mockResponse)); + + const result = await service.get(conditionId, instrumentId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if get fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should make an http post for create', async () => { + const mockRequest: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + const mockResponse: NoticeOfIntentDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.post.mockReturnValue(of(mockResponse)); + + const result = await service.create(conditionId, mockRequest); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, mockRequest); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if create fails', async () => { + const mockRequest: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.post.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.create(conditionId, mockRequest)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, mockRequest); + }); + + it('should make an http patch for update', async () => { + const mockRequest: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + const mockResponse: NoticeOfIntentDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.patch.mockReturnValue(of(mockResponse)); + + const result = await service.update(conditionId, instrumentId, mockRequest); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, mockRequest); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if update fails', async () => { + const mockRequest: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.patch.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.update(conditionId, instrumentId, mockRequest)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, mockRequest); + }); + + it('should make an http delete for delete', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.delete(conditionId, instrumentId); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should show an error if delete fails', async () => { + mockHttpClient.delete.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.delete(conditionId, instrumentId)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should remove statusDate and explanation when status is RECEIVED during update', async () => { + const mockRequest: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + statusDate: 1672531200000, + explanation: 'test', + instrumentNumber: '123', + }; + + const mockResponse: NoticeOfIntentDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.patch.mockReturnValue(of(mockResponse)); + + await service.update(conditionId, instrumentId, mockRequest); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }); + }); + + it('should throw Condition/financial instrument not found', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 404, error: { message: 'Condition/financial instrument not found' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition/financial instrument not found'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should throw Condition is not of type Financial Security', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 400, error: { message: 'Condition is not of type Financial Security' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition is not of type Financial Security'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should throw Condition type Financial Security not found', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 500, error: { message: 'Condition type Financial Security not found' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition type Financial Security not found'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); +}); diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts new file mode 100644 index 0000000000..665d7e9847 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; +import { + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, +} from './notice-of-intent-decision-condition-financial-instrument.dto'; +import { DecisionConditionFinancialInstrumentService } from '../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { environment } from '../../../../../../environments/environment'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; +import { InstrumentStatus } from '../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentDecisionConditionFinancialInstrumentService extends DecisionConditionFinancialInstrumentService { + private baseUrl = `${environment.apiUrl}/notice-of-intent-decision-condition`; + private financialInstrumentUrl = 'financial-instruments'; + + constructor(http: HttpClient) { + super(http); + } + async getAll(conditionUuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}`; + + try { + return await firstValueFrom(this.http.get(url)); + } catch (e) { + this.handleError(e); + } + } + + async get(conditionUuid: string, uuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + try { + return await firstValueFrom(this.http.get(url)); + } catch (e) { + this.handleError(e); + } + } + + async create( + conditionUuid: string, + dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}`; + + try { + return await firstValueFrom(this.http.post(url, dto)); + } catch (e) { + this.handleError(e); + } + } + + async update( + conditionUuid: string, + uuid: string, + dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + if (dto.status === InstrumentStatus.RECEIVED) { + if (dto.statusDate) { + delete dto.statusDate; + } + if (dto.explanation) { + delete dto.explanation; + } + } + + try { + return await firstValueFrom(this.http.patch(url, dto)); + } catch (e) { + this.handleError(e); + } + } + + async delete(conditionUuid: string, uuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + try { + return await firstValueFrom(this.http.delete(url)); + } catch (e) { + this.handleError(e); + } + } + + private handleError(e: any): never { + console.error(e); + let message; + if (e instanceof HttpErrorResponse) { + if (e.status === 404) { + message = 'Condition/financial instrument not found'; + } else if (e.status === 400) { + message = 'Condition is not of type Financial Security'; + } else { + if (e.error.message === 'Condition type Financial Security not found') { + message = 'Condition type Financial Security not found'; + } else { + message = 'Failed to retrieve the financial instruments'; + } + } + } else { + message = 'Failed to perform the operation'; + } + throw new Error(message); + } +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts index 281bbf3324..df551592ee 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts @@ -4,6 +4,10 @@ import { DateLabel, DateType } from '../../application/decision/application-deci import { CardDto } from '../../card/card.dto'; import { NoticeOfIntentTypeDto } from '../notice-of-intent.dto'; +export enum ConditionType { + FINANCIAL_SECURITY = 'BOND', +} + export interface UpdateNoticeOfIntentDecisionDto { resolutionNumber?: number; resolutionYear?: number; diff --git a/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.spec.ts b/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.spec.ts new file mode 100644 index 0000000000..e0e26bab5e --- /dev/null +++ b/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.spec.ts @@ -0,0 +1,48 @@ +import { TestBed } from '@angular/core/testing'; +import { AuthenticationService, ICurrentUser } from '../../authentication/authentication.service'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { DecisionOutcomeDataSourceService, TreeNode } from './decision-outcome-data-source.service'; + +describe('DecisionOutcomeDataSourceService', () => { + let service: DecisionOutcomeDataSourceService; + let mockAuthenticationService: DeepMocked; + let currentUser: BehaviorSubject; + + beforeEach(() => { + mockAuthenticationService = createMock(); + currentUser = new BehaviorSubject(undefined); + TestBed.configureTestingModule({ + providers: [ + { + provide: AuthenticationService, + useValue: mockAuthenticationService, + }, + ], + }); + mockAuthenticationService.$currentUser = currentUser; + service = TestBed.inject(DecisionOutcomeDataSourceService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should load initial data', () => { + expect(service.data).toBeTruthy(); + expect(service.data.length).toBeGreaterThan(0); + }); + + it('should filter data', () => { + service.filter('Approved'); + expect(service.data.length).toBe(1); + + const node: TreeNode = service.data[0]; + expect(node.item.label).toEqual('Approved'); + }); + + it('should reset data when filtering with empty text', () => { + service.filter(''); + expect(service.data.length).toBeGreaterThan(0); + }); +}); diff --git a/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.ts b/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.ts new file mode 100644 index 0000000000..195cfc0c5c --- /dev/null +++ b/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { AuthenticationService, ROLES } from '../../authentication/authentication.service'; + +export interface TreeNodeItem { + label: string; + value: string | null; +} +/** + * Node for to-do item + */ +export interface TreeNode { + children?: TreeNode[]; + item: TreeNodeItem; +} + +/** Flat to-do item node with expandable and level information */ +export interface FlatTreeNode { + item: TreeNodeItem; + level: number; + expandable: boolean; +} + +const TREE_DATA: TreeNode[] = [ + { + item: { label: 'Approved', value: 'APPR' }, + }, + { + item: { label: 'Refused', value: 'REFU' }, + }, + { + item: { label: 'Ordered not to Proceed (NOI)', value: 'ONTP' }, + }, +]; + +@Injectable({ providedIn: 'root' }) +export class DecisionOutcomeDataSourceService { + dataChange = new BehaviorSubject([]); + treeData: TreeNode[] = []; + isCommissioner = false; + + get data(): TreeNode[] { + return this.dataChange.value; + } + + constructor() { + this.initialize(); + } + + initialize() { + this.treeData = TREE_DATA; + this.isCommissioner ? this.dataChange.next(TREE_DATA) : this.dataChange.next(TREE_DATA); + } + + public filter(filterText: string) { + let filteredTreeData; + if (filterText) { + // Filter the tree + const filter = (array: TreeNode[], text: string) => { + const getChildren = (result: any, object: any) => { + if (object.item.label.toLowerCase().includes(text.toLowerCase())) { + result.push(object); + return result; + } + if (Array.isArray(object.children)) { + const children = object.children.reduce(getChildren, []); + if (children.length) result.push({ ...object, children }); + } + return result; + }; + + return array.reduce(getChildren, []); + }; + + filteredTreeData = filter(this.treeData, filterText); + } else { + // Return the initial tree + filteredTreeData = this.treeData; + } + this.dataChange.next(filteredTreeData); + } + + public getCommissionerListData() { + return this.treeData; + } +} diff --git a/alcs-frontend/src/app/services/search/search.dto.ts b/alcs-frontend/src/app/services/search/search.dto.ts index a99e3fe84a..1ef032c1cc 100644 --- a/alcs-frontend/src/app/services/search/search.dto.ts +++ b/alcs-frontend/src/app/services/search/search.dto.ts @@ -80,6 +80,8 @@ export interface SearchRequestDto extends PagingRequestDto { dateDecidedFrom?: number; dateDecidedTo?: number; fileTypes: string[]; + decisionMaker?: string; + decisionOutcomes?: string[]; tagIds?: string[]; tagCategoryId?: string; } diff --git a/alcs-frontend/src/app/services/user/user.dto.ts b/alcs-frontend/src/app/services/user/user.dto.ts index e5ea623f4d..810ecd8d72 100644 --- a/alcs-frontend/src/app/services/user/user.dto.ts +++ b/alcs-frontend/src/app/services/user/user.dto.ts @@ -23,7 +23,7 @@ export interface AssigneeDto { uuid: string; initials?: string; name: string; - email: string; + email?: string; mentionLabel: string; clientRoles: ROLES[]; prettyName: string; diff --git a/alcs-frontend/src/app/shared/constants.ts b/alcs-frontend/src/app/shared/constants.ts index 7e0b8ed4c4..8d29c26d0a 100644 --- a/alcs-frontend/src/app/shared/constants.ts +++ b/alcs-frontend/src/app/shared/constants.ts @@ -1 +1,7 @@ export const FILE_NAME_TRUNCATE_LENGTH = 30; +export const APPLICATION_ROUTER_LINK_BASE = 'application'; +export const NOI_ROUTER_LINK_BASE = 'notice-of-intent'; +export enum DialogAction { + ADD = 'add', + EDIT = 'edit', +} diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.html b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.html new file mode 100644 index 0000000000..0e91fa2e99 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.html @@ -0,0 +1,141 @@ +
+
+

{{ isEdit ? 'Edit Financial Instrument' : 'Add Financial Instrument' }}

+
+ +
+
+ + Security Holder/Payee + + +
+ +
+ + Type + + {{ + type.value + }} + + + + Issue Date + + + + +
+ +
+ + Amount + + + + Held By + + {{ + heldBy.value + }} + + +
+ +
+ + Bank + + + + Instrument Number + + +
+ +
+ + Received Date + + + + + + Expiry Date + + + + +
+ +
+ + Notes + + +
+ +
+
+ Select Instrument Status + + Received - instrument has been received or for EFTs, CSNR has confirmed + Released - conditions related to this security were met and security has been released + Cashed - the security will not be released + Replaced - this security was replaced by another security + +
+
+ + +
+ + Status Date + + + + +
+ +
+ + Explanation + + +
+
+
+ + + + + +
diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.scss b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.scss new file mode 100644 index 0000000000..e8ba944712 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.scss @@ -0,0 +1,72 @@ +@use '../../../../styles/colors'; + +.container { + display: flex; + flex-direction: column; + padding: 48px 36px 24px; + width: 100%; +} + +.header { + display: block; +} + +.content { + display: block; + flex-direction: column; +} + +.row { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 18px; + flex-grow: 1; + margin: 20px 0 0 0; + + & > * { + flex: 1; + } + + & > :only-child { + flex-grow: 1; + } +} + +.status-radio-group-container { + display: flex; + flex-direction: column !important; + flex: 1; +} + +.status-radio-group { + display: flex; + flex-direction: column; +} + +mat-radio-button { + margin-bottom: -8px !important; +} + +::ng-deep .mat-mdc-radio-button.mat-accent { + --mdc-radio-disabled-selected-icon-color: black; + --mdc-radio-disabled-unselected-icon-color: black; + --mdc-radio-unselected-hover-icon-color: #212121; + --mdc-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mdc-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.54); + --mdc-radio-selected-focus-icon-color: #{colors.$primary-color}; + --mdc-radio-selected-hover-icon-color: #{colors.$primary-color}; + --mdc-radio-selected-icon-color: #{colors.$primary-color}; + --mdc-radio-selected-pressed-icon-color: #{colors.$primary-color}; + --mat-radio-ripple-color: black; + --mat-radio-checked-ripple-color: #{colors.$primary-color}; + --mat-radio-disabled-label-color: rgba(0, 0, 0, 0.38); +} + +.actions-container { + display: flex; + flex-direction: row; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; +} diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts new file mode 100644 index 0000000000..a3a2d50984 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; + +import { DecisionConditionFinancialInstrumentDialogComponent } from './decision-condition-financial-instrument-dialog.component'; + +describe('DecisionConditionFinancialInstrumentDialogComponent', () => { + let component: DecisionConditionFinancialInstrumentDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DecisionConditionFinancialInstrumentDialogComponent], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionConditionFinancialInstrumentDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts new file mode 100644 index 0000000000..1e227df178 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts @@ -0,0 +1,160 @@ +import { Component, Inject, Input, OnInit, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DialogAction } from '../../constants'; +import { + DecisionConditionFinancialInstrumentDto, + CreateUpdateDecisionConditionFinancialInstrumentDto, +} from '../../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; +import { FormControl, FormGroup, ValidatorFn, Validators, AbstractControl } from '@angular/forms'; +import { + HeldBy, + InstrumentStatus, + InstrumentType, +} from '../../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; +import { DecisionConditionFinancialInstrumentService } from '../../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { ToastService } from '../../../services/toast/toast.service'; + +@Component({ + selector: 'app-decision-condition-financial-instrument-dialog', + templateUrl: './decision-condition-financial-instrument-dialog.component.html', + styleUrl: './decision-condition-financial-instrument-dialog.component.scss', +}) +export class DecisionConditionFinancialInstrumentDialogComponent implements OnInit { + @Input() service?: DecisionConditionFinancialInstrumentService; + isEdit: boolean = false; + form: FormGroup; + + securityHolderPayee = new FormControl('', Validators.required); + type = new FormControl(null, [Validators.required, this.enumValidator(InstrumentType)]); + issueDate = new FormControl(null, Validators.required); + expiryDate = new FormControl(null); + amount = new FormControl(null, Validators.required); + bank = new FormControl('', Validators.required); + instrumentNumber = new FormControl(null, Validators.required); + heldBy = new FormControl(null, [Validators.required, this.enumValidator(HeldBy)]); + receivedDate = new FormControl(null, Validators.required); + notes = new FormControl(null); + status = new FormControl(InstrumentStatus.RECEIVED, [ + Validators.required, + this.enumValidator(InstrumentStatus), + ]); + statusDate = new FormControl(null); + explanation = new FormControl(null); + + instrumentTypes = InstrumentType; + heldByOptions = HeldBy; + instrumentStatuses = InstrumentStatus; + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: { + action: DialogAction; + conditionUuid: string; + instrument?: DecisionConditionFinancialInstrumentDto; + service: DecisionConditionFinancialInstrumentService; + }, + private dialogRef: MatDialogRef, + private toastService: ToastService, + ) { + this.form = new FormGroup({ + securityHolderPayee: this.securityHolderPayee, + type: this.type, + issueDate: this.issueDate, + expiryDate: this.expiryDate, + amount: this.amount, + bank: this.bank, + instrumentNumber: this.instrumentNumber, + heldBy: this.heldBy, + receivedDate: this.receivedDate, + notes: this.notes, + status: this.status, + statusDate: this.statusDate, + explanation: this.explanation, + }); + } + + ngOnInit(): void { + this.service = this.data.service; + this.isEdit = this.data.action === DialogAction.EDIT; + + this.form.get('status')?.valueChanges.subscribe((status) => { + if (status === InstrumentStatus.RECEIVED) { + this.form.get('statusDate')?.setValidators([]); + this.form.get('explanation')?.setValidators([]); + } else { + this.form.get('statusDate')?.setValidators([Validators.required]); + this.form.get('explanation')?.setValidators([Validators.required]); + } + + this.form.get('statusDate')?.updateValueAndValidity(); + this.form.get('explanation')?.updateValueAndValidity(); + }); + + this.form.get('type')?.valueChanges.subscribe((type) => { + if (type === InstrumentType.EFT) { + this.form.get('instrumentNumber')?.setValidators([]); + } else { + this.form.get('instrumentNumber')?.setValidators([Validators.required]); + } + this.form.get('instrumentNumber')?.updateValueAndValidity(); + }); + + if (this.isEdit && this.data.instrument) { + const instrument = this.data.instrument; + this.form.patchValue({ + ...instrument, + issueDate: instrument.issueDate ? new Date(instrument.issueDate) : null, + expiryDate: instrument.expiryDate ? new Date(instrument.expiryDate) : null, + receivedDate: instrument.receivedDate ? new Date(instrument.receivedDate) : null, + statusDate: instrument.statusDate ? new Date(instrument.statusDate) : null, + }); + } + } + + enumValidator(enumType: any): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + if (!Object.values(enumType).includes(control.value)) { + return { enum: { value: control.value } }; + } + return null; + }; + } + + mapToDto() { + const formData = this.form.getRawValue(); + const dto: CreateUpdateDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: formData.securityHolderPayee, + type: formData.type, + issueDate: new Date(formData.issueDate).getTime(), + expiryDate: formData.expiryDate ? new Date(formData.expiryDate).getTime() : null, + amount: formData.amount, + bank: formData.bank, + instrumentNumber: formData.instrumentNumber, + heldBy: formData.heldBy, + receivedDate: new Date(formData.receivedDate).getTime(), + notes: formData.notes, + status: formData.status, + statusDate: formData.statusDate ? new Date(formData.statusDate).getTime() : null, + explanation: formData.explanation, + }; + return dto; + } + + async onSubmit() { + try { + const dto = this.mapToDto(); + if (this.isEdit && this.data.instrument?.uuid) { + await this.service!.update(this.data.conditionUuid, this.data.instrument.uuid, dto); + this.toastService.showSuccessToast('Financial Instrument updated successfully'); + } else { + await this.service!.create(this.data.conditionUuid, dto); + this.toastService.showSuccessToast('Financial Instrument created successfully'); + } + this.dialogRef.close({ action: this.isEdit ? DialogAction.EDIT : DialogAction.ADD, successful: true }); + } catch (error: any) { + console.error(error); + this.toastService.showErrorToast(error.message || 'An error occurred'); + this.dialogRef.close({ action: this.isEdit ? DialogAction.EDIT : DialogAction.ADD, successful: false }); + } + } +} diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html new file mode 100644 index 0000000000..19f27977e6 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html @@ -0,0 +1,70 @@ +
+
+
Instrument
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Amount{{ element.amount | number }}Type{{ element.type }}Bank{{ element.bank }}Instrument # + + {{ element.instrumentNumber }} + Received Date + {{ element.receivedDate | date: 'yyyy-MMM-dd' }} + Status{{ getFormattedStatus(element) }}Action + + +
+
+
+
diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss new file mode 100644 index 0000000000..4794e44c1b --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss @@ -0,0 +1,68 @@ +.condition-instrument-container { + width: 100%; + display: flex; + flex-direction: column; + border-radius: 8px; + box-shadow: 0 2px 8px 1px #00000040; + padding: 16px 12px; +} + +.header-container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: baseline; +} + +.header { + flex-grow: 1; +} + +.table-container { + overflow-x: hidden; + + @media screen and (max-width: 1152px) { + overflow-x: scroll; + } +} + +.instruments-table { + width: 100%; + border-spacing: 0; + background-color: inherit; + + th.mat-header-cell, + td.mat-cell { + padding: 2px; + border-bottom: 1px solid #ddd; + text-align: left; + } + + .column-amount { + width: 10%; + } + + .column-type { + width: 15%; + } + + .column-bank { + width: 10%; + } + + .column-instrument-number { + width: 15%; + } + + .column-received-date { + width: 15%; + } + + .column-status { + width: 20%; + } + + .column-action { + width: 15%; + } +} diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts new file mode 100644 index 0000000000..0af13d36da --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DecisionConditionFinancialInstrumentComponent } from './decision-condition-financial-instrument.component'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { HttpClient } from '@angular/common/http'; +import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; + +describe('DecisionConditionFinancialInstrumentComponent', () => { + let component: DecisionConditionFinancialInstrumentComponent; + let fixture: ComponentFixture; + let mockHttpClient: DeepMocked; + let mockFinancialInstrumentService: DeepMocked; + + beforeEach(async () => { + mockHttpClient = createMock(); + mockFinancialInstrumentService = createMock(); + await TestBed.configureTestingModule({ + declarations: [DecisionConditionFinancialInstrumentComponent], + providers: [ + { provide: HttpClient, useValue: mockHttpClient }, + { provide: DecisionConditionFinancialInstrumentService, useValue: mockFinancialInstrumentService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionConditionFinancialInstrumentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts new file mode 100644 index 0000000000..08db4359c7 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts @@ -0,0 +1,115 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSort, Sort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { + DecisionConditionFinancialInstrumentDto, + InstrumentStatus, +} from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; +import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { DialogAction } from '../constants'; +import { ConfirmationDialogService } from '../confirmation-dialog/confirmation-dialog.service'; +import { ToastService } from '../../services/toast/toast.service'; +import { DecisionConditionFinancialInstrumentDialogComponent } from './decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component'; + +@Component({ + selector: 'app-decision-condition-financial-instrument', + templateUrl: './decision-condition-financial-instrument.component.html', + styleUrl: './decision-condition-financial-instrument.component.scss', +}) +export class DecisionConditionFinancialInstrumentComponent implements OnInit { + @Input() conditionUuid!: string; + + displayColumns: string[] = ['Amount', 'Type', 'Bank', 'Instrument #', 'Received Date', 'Status', 'Action']; + @ViewChild(MatSort) sort!: MatSort; + dataSource: MatTableDataSource = new MatTableDataSource(); + + instruments: DecisionConditionFinancialInstrumentDto[] = []; + + constructor( + private dialog: MatDialog, + private financialInstrumentService: DecisionConditionFinancialInstrumentService, + private confirmationDialogService: ConfirmationDialogService, + private toastService: ToastService, + ) {} + + ngOnInit(): void { + this.initData(); + } + + async initData() { + this.instruments = await this.financialInstrumentService.getAll(this.conditionUuid); + this.instruments.sort((a, b) => (a.receivedDate < b.receivedDate ? -1 : 1)); + this.dataSource.data = this.instruments; + } + + onAddInstrument(): void { + const dialogRef = this.dialog.open(DecisionConditionFinancialInstrumentDialogComponent, { + minWidth: '800px', + maxWidth: '1100px', + maxHeight: '80vh', + data: { + conditionUuid: this.conditionUuid, + action: DialogAction.ADD, + service: this.financialInstrumentService, + }, + }); + + dialogRef.afterClosed().subscribe((result: { action: DialogAction; successful: boolean }) => { + if (result.successful) { + this.initData(); + } + }); + } + + onEditInstrument(instrument: DecisionConditionFinancialInstrumentDto): void { + const dialogRef = this.dialog.open(DecisionConditionFinancialInstrumentDialogComponent, { + minWidth: '800px', + maxWidth: '1100px', + maxHeight: '80vh', + data: { + conditionUuid: this.conditionUuid, + action: DialogAction.EDIT, + instrument: instrument, + service: this.financialInstrumentService, + }, + }); + + dialogRef.afterClosed().subscribe((result: { action: DialogAction; successful: boolean }) => { + if (result.successful) { + this.initData(); + } + }); + } + + onDeleteInstrument(instrument: DecisionConditionFinancialInstrumentDto): void { + this.confirmationDialogService + .openDialog({ body: 'Are you sure you want to delete this instrument?' }) + .subscribe(async (confirmed) => { + if (confirmed) { + try { + await this.financialInstrumentService.delete(this.conditionUuid, instrument.uuid); + this.toastService.showSuccessToast('Instrument successfully deleted'); + this.initData(); + } catch (e) { + this.toastService.showErrorToast('Failed to delete the instrument'); + } + } + }); + } + + getFormattedStatus(instrument: DecisionConditionFinancialInstrumentDto): string { + if (instrument.status === InstrumentStatus.RECEIVED) { + return instrument.status; + } + + if (instrument.statusDate) { + const date = new Date(instrument.statusDate); + const year = date.getFullYear(); + const month = date.toLocaleDateString('en-CA', { month: 'short' }); + const day = String(date.getDate()).padStart(2, '0'); + return `${instrument.status} on ${year}-${month}-${day}`; + } + return ''; + } +} diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html index 0931a4cc91..a4bcb11c6c 100644 --- a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html @@ -86,23 +86,23 @@

{{ title }} Document

-
+
Associated Parcel - - #{{ parcel.index + 1 }} PID: + + #{{ i + 1 }} PID: {{ parcel.pid | mask: '000-000-000' }} No Data
-
+
Associated Organization - + {{ owner.label }} diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts index 8dcd3d988c..517d01bc9e 100644 --- a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts @@ -3,29 +3,23 @@ import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@ang import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { ToastService } from '../../services/toast/toast.service'; -import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, DOCUMENT_TYPE, DocumentTypeDto } from '../document/document.dto'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../document/document.dto'; import { FileHandle } from '../drag-drop-file/drag-drop-file.directive'; import { splitExtension } from '../utils/file'; -import { DecisionService, DocumentService } from './document-upload-dialog.interface'; import { CreateDocumentDto, - DocumentDto, SelectableOwnerDto, SelectableParcelDto, UpdateDocumentDto, } from './document-upload-dialog.dto'; import { Subject } from 'rxjs'; +import { DocumentUploadDialogData } from './document-upload-dialog.interface'; export enum VisibilityGroup { INTERNAL = 'Internal', PUBLIC = 'Public', } -export interface DocumentTypeConfig { - visibilityGroups: VisibilityGroup[]; - allowsFileEdit: boolean; -} - @Component({ selector: 'app-document-upload-dialog', templateUrl: './document-upload-dialog.component.html', @@ -74,20 +68,12 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { internalVisibilityLabel = ''; + selectableParcels: SelectableParcelDto[] = []; + selectableOwners: SelectableOwnerDto[] = []; + constructor( @Inject(MAT_DIALOG_DATA) - public data: { - fileId: string; - decisionUuid?: string; - existingDocument?: DocumentDto; - decisionService?: DecisionService; - documentService?: DocumentService; - selectableParcels?: SelectableParcelDto[]; - selectableOwners?: SelectableOwnerDto[]; - allowedVisibilityFlags?: ('A' | 'C' | 'G' | 'P')[]; - allowsFileEdit?: boolean; - documentTypeOverrides?: Record; - }, + public data: DocumentUploadDialogData, protected dialog: MatDialogRef, private toastService: ToastService, ) {} @@ -104,7 +90,7 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { this.allowsFileEdit = this.data.allowsFileEdit ?? this.allowsFileEdit; if (document.type && this.data.documentTypeOverrides && this.data.documentTypeOverrides[document.type.code]) { - this.allowsFileEdit = this.data.documentTypeOverrides[document.type.code].allowsFileEdit; + this.allowsFileEdit = !!this.data.documentTypeOverrides[document.type.code]?.allowsFileEdit; } if (document.type?.code === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { @@ -267,32 +253,54 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { } async prepareCertificateOfTitleUpload(uuid?: string) { - if (this.data.selectableParcels && this.data.selectableParcels.length > 0) { - this.parcelId.setValidators([Validators.required]); - this.parcelId.updateValueAndValidity(); - this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + this.parcelId.setValidators([Validators.required]); + this.parcelId.updateValueAndValidity(); - const selectedParcel = this.data.selectableParcels.find((parcel) => parcel.certificateOfTitleUuid === uuid); - if (selectedParcel) { - this.parcelId.setValue(selectedParcel.uuid); - } else if (uuid) { - this.showSupersededWarning = true; - } + if (!this.data.parcelService) { + return; + } + + this.selectableParcels = await this.data.parcelService.fetchParcels(this.data.fileId); + + if (this.selectableParcels.length < 1) { + return; + } + + const selectedParcel = this.selectableParcels.find((parcel) => parcel.certificateOfTitleUuid === uuid); + if (selectedParcel) { + this.parcelId.setValue(selectedParcel.uuid); + } else if (uuid) { + this.showSupersededWarning = true; } } async prepareCorporateSummaryUpload(uuid?: string) { - if (this.data.selectableOwners && this.data.selectableOwners.length > 0) { - this.ownerId.setValidators([Validators.required]); - this.ownerId.updateValueAndValidity(); - this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + this.ownerId.setValidators([Validators.required]); + this.ownerId.updateValueAndValidity(); - const selectedOwner = this.data.selectableOwners.find((owner) => owner.corporateSummaryUuid === uuid); - if (selectedOwner) { - this.ownerId.setValue(selectedOwner.uuid); - } else if (uuid) { - this.showSupersededWarning = true; - } + if (!this.data.submissionService) { + return; + } + + const submission = await this.data.submissionService.fetchSubmission(this.data.fileId); + this.selectableOwners = submission.owners + .filter((owner) => owner.type.code === 'ORGZ') + .map((owner) => ({ + ...owner, + label: owner.organizationName ?? owner.displayName, + })); + + if (this.selectableOwners.length < 1) { + return; + } + + const selectedOwner = this.selectableOwners.find((owner) => owner.corporateSummaryUuid === uuid); + if (selectedOwner) { + this.ownerId.setValue(selectedOwner.uuid); + } else if (uuid) { + this.showSupersededWarning = true; } } @@ -302,7 +310,7 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { } if (this.data.documentTypeOverrides && this.data.documentTypeOverrides[$event.code]) { - for (const visibilityGroup of this.data.documentTypeOverrides[$event.code].visibilityGroups) { + for (const visibilityGroup of this.data.documentTypeOverrides[$event.code]?.visibilityGroups ?? []) { if (visibilityGroup === VisibilityGroup.INTERNAL) { this.visibleToInternal.setValue(true); } diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts index 73bb4085ff..3f731540c3 100644 --- a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts @@ -1,4 +1,5 @@ import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, DOCUMENT_TYPE, DocumentTypeDto } from '../../shared/document/document.dto'; +import { BaseCodeDto } from '../dto/base.dto'; export interface UpdateDocumentDto { file?: File; @@ -32,13 +33,24 @@ export interface DocumentDto { export interface SelectableParcelDto { uuid: string; - pid: string; - certificateOfTitleUuid: string; - index: string; + pid?: string; + certificateOfTitleUuid?: string; +} + +export interface OwnerDto { + uuid: string; + displayName: string; + organizationName?: string | null; + corporateSummaryUuid?: string; + type: BaseCodeDto; } export interface SelectableOwnerDto { - label: string; uuid: string; - corporateSummaryUuid: string; + corporateSummaryUuid?: string; + label: string; +} + +export interface SubmissionOwnersDto { + owners: OwnerDto[]; } diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts index 34e7f4dbc5..70e249a65f 100644 --- a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts @@ -1,5 +1,33 @@ -import { DocumentTypeDto } from '../document/document.dto'; -import { CreateDocumentDto, UpdateDocumentDto } from './document-upload-dialog.dto'; +import { DOCUMENT_TYPE, DocumentTypeDto } from '../document/document.dto'; +import { VisibilityGroup } from './document-upload-dialog.component'; +import { + CreateDocumentDto, + DocumentDto, + SelectableParcelDto, + SubmissionOwnersDto, + UpdateDocumentDto, +} from './document-upload-dialog.dto'; + +export interface DocumentTypeConfig { + visibilityGroups: VisibilityGroup[]; + allowsFileEdit: boolean; +} + +export interface DocumentUploadDialogOptions { + allowedVisibilityFlags?: ('A' | 'C' | 'G' | 'P')[]; + allowsFileEdit?: boolean; + documentTypeOverrides?: Partial>; +} + +export interface DocumentUploadDialogData extends DocumentUploadDialogOptions { + fileId: string; + decisionUuid?: string; + existingDocument?: DocumentDto; + decisionService?: DecisionService; + documentService?: DocumentService; + parcelService?: ParcelFetchingService; + submissionService?: SubmissionFetchingService; +} export interface DecisionService { uploadFile(decisionUuid: string, file: File): Promise; @@ -15,3 +43,11 @@ export interface DocumentService { fetchTypes(): Promise; delete(uuid: string): Promise; } + +export interface ParcelFetchingService { + fetchParcels(fileNumber: string): Promise; +} + +export interface SubmissionFetchingService { + fetchSubmission(fileNumber: string): Promise; +} diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.html index b3031b168b..b1905ec169 100644 --- a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.html +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.html @@ -1,8 +1,13 @@
- {{ placeholder}} - {{ value }} + {{ placeholder }} + +
{{ value }}
+
+ + {{ value }} +
edit @@ -11,7 +16,7 @@
diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.scss index 2e72748af2..b68ec801d1 100644 --- a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.scss +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.scss @@ -67,3 +67,7 @@ .save { color: colors.$primary-color; } + +.pre-wrap { + white-space: pre-wrap; +} diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.ts index 5f6daba83c..61237cfe87 100644 --- a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.ts +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.ts @@ -8,6 +8,7 @@ import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@ export class InlineTextareaEditComponent { @Input() value: string = ''; @Input() placeholder: string = 'Enter a value'; + @Input() allowParagraphs: boolean = false; @Output() save = new EventEmitter(); @ViewChild('editInput') textInput!: ElementRef; diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index 28ea05cc7a..6e2cd35cc9 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -82,6 +82,8 @@ import { CommissionerTagsHeaderComponent } from './tags/commissioner-tags-header import { DocumentUploadDialogComponent } from './document-upload-dialog/document-upload-dialog.component'; import { FlagDialogComponent } from './flag-dialog/flag-dialog.component'; import { UnFlagDialogComponent } from './unflag-dialog/unflag-dialog.component'; +import { DecisionConditionFinancialInstrumentComponent } from './decision-condition-financial-instrument/decision-condition-financial-instrument.component'; +import { DecisionConditionFinancialInstrumentDialogComponent } from './decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component'; @NgModule({ declarations: [ @@ -129,6 +131,8 @@ import { UnFlagDialogComponent } from './unflag-dialog/unflag-dialog.component'; DocumentUploadDialogComponent, FlagDialogComponent, UnFlagDialogComponent, + DecisionConditionFinancialInstrumentComponent, + DecisionConditionFinancialInstrumentDialogComponent, ], imports: [ CommonModule, @@ -157,6 +161,7 @@ import { UnFlagDialogComponent } from './unflag-dialog/unflag-dialog.component'; MatChipsModule, MatAutocompleteModule, MatCheckboxModule, + MatRadioModule, ], exports: [ CommonModule, @@ -236,6 +241,8 @@ import { UnFlagDialogComponent } from './unflag-dialog/unflag-dialog.component'; DocumentUploadDialogComponent, FlagDialogComponent, UnFlagDialogComponent, + DecisionConditionFinancialInstrumentComponent, + DecisionConditionFinancialInstrumentDialogComponent, ], }) export class SharedModule { diff --git a/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html b/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html index 7502e487f3..7e792df5ac 100644 --- a/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html +++ b/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html @@ -1,8 +1,3 @@ -
- +
+
diff --git a/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.scss b/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.scss index 72d84b961d..25dae12f3f 100644 --- a/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.scss +++ b/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.scss @@ -5,14 +5,6 @@ border: 1px solid transparent; border-radius: 4px; padding: 5px; - - &.hovered { - border: 1px solid #aaaaaa; - } - - &.clicked { - border: 1px solid #929292; - } } .category { diff --git a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html index 78a441da7a..b6636ed0db 100644 --- a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html +++ b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html @@ -1,6 +1,14 @@ -{{ tag.name }} + + {{ tag.name }} - + diff --git a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.scss b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.scss index 8caca14e56..9cb6d7f732 100644 --- a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.scss +++ b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.scss @@ -1,4 +1,4 @@ -mat-chip-row { +mat-chip { border-radius: 4px; border-color: #929292 !important; border: 1px solid; @@ -8,3 +8,31 @@ mat-chip-row { .removable { margin: 4px; } + +.commissioner { + pointer-events: none !important; + + &, + &:hover, + &:active, + &:focus { + border-color: #929292 !important; + background-color: #f3f3f3 !important; + box-shadow: none !important; + cursor: default !important; + } + + &::before, + &::after { + display: none !important; + } + + ::ng-deep .mat-mdc-chip-action, + ::ng-deep .mdc-evolution-chip__action { + pointer-events: none !important; + } + + * { + transition: none !important; + } +} diff --git a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.ts b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.ts index 6b4ef6f68d..34b50933b7 100644 --- a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.ts +++ b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.ts @@ -9,9 +9,15 @@ import { TagDto } from '../../../services/tag/tag.dto'; export class TagChipComponent { @Input() tag!: TagDto; @Input() removable: boolean = true; + @Input() isCommissioner: boolean = false; @Output() removeClicked = new EventEmitter(); onRemove() { this.removeClicked.emit(this.tag); } + + handleClickOrKeyPress(event: MouseEvent | KeyboardEvent) { + event.preventDefault(); + event.stopPropagation(); + } } diff --git a/alcs-frontend/src/styles/ngselect.scss b/alcs-frontend/src/styles/ngselect.scss index 1c0292cd5d..2f0e831112 100644 --- a/alcs-frontend/src/styles/ngselect.scss +++ b/alcs-frontend/src/styles/ngselect.scss @@ -16,7 +16,7 @@ } .ng-select .ng-arrow { - color: colors.$primary-color !important; + color: colors.$primary-color; } .ng-select.ng-select-focused .ng-placeholder { @@ -53,6 +53,10 @@ height: calc(100% - 0.15em); } +.ng-select.ng-select-disabled>.ng-select-container.ng-appearance-outline:after { + border-color: colors.$grey-light !important; +} + .ng-select .ng-select-container .ng-value-container { border-top: 0.5em !important; padding: 1.4em 0 1em !important; diff --git a/portal-frontend/Dockerfile b/portal-frontend/Dockerfile index 410a505cd2..212720b0e3 100644 --- a/portal-frontend/Dockerfile +++ b/portal-frontend/Dockerfile @@ -13,7 +13,7 @@ RUN npm ci # Copy the source code to the /app directory COPY . . -ENV NODE_OPTIONS "--max-old-space-size=2048" +ENV NODE_OPTIONS="--max-old-space-size=2048" # Build the application RUN npm run build -- --output-path=dist --output-hashing=all @@ -47,7 +47,7 @@ COPY --from=build /app/dist /usr/share/nginx/html RUN chmod -R go+rwx /usr/share/nginx/html/assets # provide dynamic scp content-src -ENV ENABLED_CONNECT_SRC " 'self' http://localhost:* nrs.objectstore.gov.bc.ca" +ENV ENABLED_CONNECT_SRC=" 'self' http://localhost:* nrs.objectstore.gov.bc.ca" # When the container starts, replace the settings.json with values from environment variables ENTRYPOINT [ "./init.sh" ] diff --git a/portal-frontend/src/app/features/public/search/public-search.component.ts b/portal-frontend/src/app/features/public/search/public-search.component.ts index e2a70dae2e..e5a5bcc18a 100644 --- a/portal-frontend/src/app/features/public/search/public-search.component.ts +++ b/portal-frontend/src/app/features/public/search/public-search.component.ts @@ -270,7 +270,7 @@ export class PublicSearchComponent implements OnInit, OnDestroy { fileNumber: this.formatStringSearchParam(searchControls.fileNumber.value), name: this.formatStringSearchParam(searchControls.name.value), civicAddress: this.formatStringSearchParam(searchControls.civicAddress.value), - decisionOutcome: searchControls.portalDecisionOutcome.value ?? undefined, + decisionOutcomes: searchControls.portalDecisionOutcome.value ?? undefined, pid: this.formatStringSearchParam(searchControls.pid.value), portalStatusCodes: searchControls.portalStatus.value ?? undefined, governmentName: this.formatStringSearchParam(searchControls.government.value), @@ -461,7 +461,7 @@ export class PublicSearchComponent implements OnInit, OnDestroy { civicAddress.setValue(storedSearch.civicAddress); government.setValue(storedSearch.governmentName); portalStatus.setValue(storedSearch.portalStatusCodes ?? null); - portalDecisionOutcome.setValue(storedSearch.decisionOutcome ?? null); + portalDecisionOutcome.setValue(storedSearch.decisionOutcomes ?? null); if (storedSearch.dateDecidedTo) { dateDecidedTo.setValue(new Date(storedSearch.dateDecidedTo)); diff --git a/portal-frontend/src/app/services/search/search.dto.ts b/portal-frontend/src/app/services/search/search.dto.ts index 97ef646f4f..a78be0dd8a 100644 --- a/portal-frontend/src/app/services/search/search.dto.ts +++ b/portal-frontend/src/app/services/search/search.dto.ts @@ -49,7 +49,7 @@ export interface SearchRequestDto extends PagingRequestDto { dateDecidedFrom?: number; dateDecidedTo?: number; fileTypes: string[]; - decisionOutcome?: string[]; + decisionOutcomes?: string[]; } export const displayedColumns = ['fileId', 'ownerName', 'type', 'portalStatus', 'lastUpdate', 'government']; \ No newline at end of file diff --git a/services/Dockerfile b/services/Dockerfile index d68aea7bf4..303b1d7e83 100644 --- a/services/Dockerfile +++ b/services/Dockerfile @@ -52,7 +52,7 @@ WORKDIR /opt/app-root/ RUN chmod og+rwx /opt/app-root/ /var/run ARG environment=production -ENV NODE_ENV ${environment} +ENV NODE_ENV=${environment} COPY --from=build /opt/app-root/node_modules ./node_modules COPY --from=build /opt/app-root/dist/apps/${NEST_APP} ./dist diff --git a/services/Dockerfile.migrate b/services/Dockerfile.migrate index 5ebb2fbfe7..e6de9126ce 100644 --- a/services/Dockerfile.migrate +++ b/services/Dockerfile.migrate @@ -8,7 +8,7 @@ WORKDIR /opt/app-root/ # OpenShift fixes RUN chmod og+rwx /opt/app-root/ /var/run -ENV NPM_CONFIG_USERCONFIG /opt/app-root/.npmrc +ENV NPM_CONFIG_USERCONFIG=/opt/app-root/.npmrc RUN npm config set cache $/opt/app-root/.npm COPY package*.json ./ @@ -17,10 +17,10 @@ RUN npm ci ARG environment=production -ENV NODE_ENV ${environment} +ENV NODE_ENV=${environment} ARG NEST_APP=alcs -ENV NEST_APP ${NEST_APP} +ENV NEST_APP=${NEST_APP} COPY . . diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.spec.ts index ff8d46bcc4..7c68070ea8 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.spec.ts @@ -121,8 +121,8 @@ describe('ApplicationDecisionConditionCardController', () => { conditionCard.decision = { uuid: 'decision-uuid', application: { fileNumber: 'file-number' } } as any; mockService.getByBoardCard.mockResolvedValue(conditionCard); - mockReconsiderationService.getByApplicationDecisionUuid.mockResolvedValue([]); - mockModificationService.getByApplicationDecisionUuid.mockResolvedValue([]); + mockReconsiderationService.getByApplication.mockResolvedValue([]); + mockModificationService.getByApplication.mockResolvedValue([]); mockApplicationDecisionService.getDecisionOrder.mockResolvedValue(1); const result = await controller.getByCardUuid(uuid); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.ts index 0b728caeaf..422052b57c 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.ts @@ -68,12 +68,8 @@ export class ApplicationDecisionConditionCardController { ); dto.fileNumber = result.decision.application.fileNumber; - const appModifications = await this.applicationModificationService.getByApplicationDecisionUuid( - result.decision.uuid, - ); - const appReconsiderations = await this.applicationReconsiderationService.getByApplicationDecisionUuid( - result.decision.uuid, - ); + const appModifications = await this.applicationModificationService.getByApplication(dto.fileNumber); + const appReconsiderations = await this.applicationReconsiderationService.getByApplication(dto.fileNumber); dto.isModification = appModifications.length > 0; dto.isReconsideration = appReconsiderations.length > 0; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service.ts index 71975b13c0..447bd5843d 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service.ts @@ -191,10 +191,8 @@ export class ApplicationDecisionConditionCardService { }); for (const dto of dtos) { - const appModifications = await this.applicationModificationService.getByApplicationDecisionUuid(dto.decisionUuid); - const appReconsiderations = await this.applicationReconsiderationService.getByApplicationDecisionUuid( - dto.decisionUuid, - ); + const appModifications = await this.applicationModificationService.getByApplication(dto.fileNumber); + const appReconsiderations = await this.applicationReconsiderationService.getByApplication(dto.fileNumber); dto.isModification = appModifications.length > 0; dto.isReconsideration = appReconsiderations.length > 0; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts new file mode 100644 index 0000000000..9e6fa49cbf --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts @@ -0,0 +1,71 @@ +import { AutoMap } from 'automapper-classes'; +import { InstrumentType, HeldBy, InstrumentStatus } from './application-decision-condition-financial-instrument.entity'; +import { IsEnum, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; +import { OmitType } from '@nestjs/mapped-types'; + +export class ApplicationDecisionConditionFinancialInstrumentDto { + @AutoMap() + @IsUUID() + uuid: string; + + @AutoMap() + @IsString() + securityHolderPayee: string; + + @AutoMap() + @IsEnum(InstrumentType) + type: InstrumentType; + + @AutoMap() + @IsNumber() + issueDate: number; + + @AutoMap() + @IsNumber() + @IsOptional() + expiryDate?: number | null; + + @AutoMap() + @IsNumber() + amount: number; + + @AutoMap() + @IsString() + bank: string; + + @AutoMap() + @IsOptional() + instrumentNumber?: string | null; + + @AutoMap() + @IsEnum(HeldBy) + heldBy: HeldBy; + + @AutoMap() + @IsNumber() + receivedDate: number; + + @AutoMap() + @IsString() + @IsOptional() + notes?: string | null; + + @AutoMap() + @IsEnum(InstrumentStatus) + status: InstrumentStatus; + + @AutoMap() + @IsOptional() + @IsNumber() + statusDate?: number | null; + + @AutoMap() + @IsString() + @IsOptional() + explanation?: string | null; +} + +export class CreateUpdateApplicationDecisionConditionFinancialInstrumentDto extends OmitType( + ApplicationDecisionConditionFinancialInstrumentDto, + ['uuid'] as const, +) {} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts new file mode 100644 index 0000000000..2f95c3f7dd --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts @@ -0,0 +1,97 @@ +import { Entity, Column, ManyToOne, OneToMany } from 'typeorm'; +import { Base } from '../../../../common/entities/base.entity'; +import { ApplicationDecisionCondition } from '../application-decision-condition.entity'; +import { AutoMap } from 'automapper-classes'; +import { ColumnNumericTransformer } from '../../../../utils/column-numeric-transform'; + +export enum InstrumentType { + BANK_DRAFT = 'Bank Draft', + CERTIFIED_CHEQUE = 'Certified Cheque', + EFT = 'EFT', + IRREVOCABLE_LETTER_OF_CREDIT = 'Irrevocable Letter of Credit', + OTHER = 'Other', + SAFEKEEPING_AGREEMENT = 'Safekeeping Agreement', +} + +export enum HeldBy { + ALC = 'ALC', + MINISTRY = 'Ministry', +} + +export enum InstrumentStatus { + RECEIVED = 'Received', + RELEASED = 'Released', + CASHED = 'Cashed', + REPLACED = 'Replaced', +} + +@Entity({ comment: 'Instrument for Financial Security Conditions' }) +export class ApplicationDecisionConditionFinancialInstrument extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @Column({ type: 'varchar', nullable: false }) + securityHolderPayee: string; + + @AutoMap() + @Column({ type: 'enum', enum: InstrumentType, nullable: false, enumName: 'application_instrument_type' }) + type: InstrumentType; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: false }) + issueDate: Date; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: true }) + expiryDate?: Date | null; + + @AutoMap() + @Column({ type: 'decimal', precision: 12, scale: 2, nullable: false, transformer: new ColumnNumericTransformer() }) + amount: number; + + @AutoMap() + @Column({ type: 'varchar', nullable: false }) + bank: string; + + @AutoMap() + @Column({ type: 'varchar', nullable: true }) + instrumentNumber: string | null; + + @AutoMap() + @Column({ type: 'enum', enum: HeldBy, nullable: false, enumName: 'application_instrument_held_by' }) + heldBy: HeldBy; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: false }) + receivedDate: Date; + + @AutoMap() + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @AutoMap() + @Column({ + type: 'enum', + enum: InstrumentStatus, + default: InstrumentStatus.RECEIVED, + nullable: false, + enumName: 'application_instrument_status', + }) + status: InstrumentStatus; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: true }) + statusDate?: Date | null; + + @AutoMap() + @Column({ type: 'text', nullable: true }) + explanation?: string | null; + + @ManyToOne(() => ApplicationDecisionCondition, (condition) => condition.financialInstruments, { onDelete: 'CASCADE' }) + condition: ApplicationDecisionCondition; +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts new file mode 100644 index 0000000000..e10397019e --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts @@ -0,0 +1,227 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApplicationDecisionConditionFinancialInstrumentService } from './application-decision-condition-financial-instrument.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { + ApplicationDecisionConditionFinancialInstrument, + HeldBy, + InstrumentStatus, + InstrumentType, +} from './application-decision-condition-financial-instrument.entity'; +import { ApplicationDecisionCondition } from '../application-decision-condition.entity'; +import { ApplicationDecisionConditionType } from '../application-decision-condition-code.entity'; +import { Repository } from 'typeorm'; +import { CreateUpdateApplicationDecisionConditionFinancialInstrumentDto } from './application-decision-condition-financial-instrument.dto'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { + ServiceInternalErrorException, + ServiceNotFoundException, + ServiceValidationException, +} from '../../../../../../../libs/common/src/exceptions/base.exception'; +import { + initApplicationDecisionConditionFinancialInstrumentMockEntity, + initApplicationDecisionConditionTypeMockEntity, +} from '../../../../../test/mocks/mockEntities'; + +describe('ApplicationDecisionConditionFinancialInstrumentService', () => { + let service: ApplicationDecisionConditionFinancialInstrumentService; + let mockRepository: DeepMocked>; + let mockConditionRepository: DeepMocked>; + let mockConditionTypeRepository: DeepMocked>; + let mockApplicationDecisionConditionType; + let mockApplicationDecisionConditionFinancialInstrument; + + beforeEach(async () => { + mockRepository = createMock(); + mockConditionRepository = createMock(); + mockConditionTypeRepository = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ApplicationDecisionConditionFinancialInstrumentService, + { + provide: getRepositoryToken(ApplicationDecisionConditionFinancialInstrument), + useValue: mockRepository, + }, + { + provide: getRepositoryToken(ApplicationDecisionCondition), + useValue: mockConditionRepository, + }, + { + provide: getRepositoryToken(ApplicationDecisionConditionType), + useValue: mockConditionTypeRepository, + }, + ], + }).compile(); + + service = module.get( + ApplicationDecisionConditionFinancialInstrumentService, + ); + + mockApplicationDecisionConditionType = initApplicationDecisionConditionTypeMockEntity('BOND'); + mockApplicationDecisionConditionFinancialInstrument = + initApplicationDecisionConditionFinancialInstrumentMockEntity(); + }); + + describe('getAll', () => { + it('should return all financial instruments for a condition', async () => { + const conditionUuid = 'condition-uuid'; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstruments = [mockApplicationDecisionConditionFinancialInstrument]; + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.find.mockResolvedValue(financialInstruments); + + const result = await service.getAll(conditionUuid); + + expect(result).toEqual(financialInstruments); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ where: { code: 'BOND' } }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.find).toHaveBeenCalledWith({ where: { condition: { uuid: conditionUuid } } }); + }); + + it('should throw an error if condition type does not exist', async () => { + mockConditionTypeRepository.findOne.mockResolvedValue(null); + + await expect(service.getAll('condition-uuid')).rejects.toThrow(ServiceInternalErrorException); + }); + + it('should throw an error if condition is not found', async () => { + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(null); + + await expect(service.getAll('condition-uuid')).rejects.toThrow(ServiceNotFoundException); + }); + + it('should throw an error if condition is not of type Financial Security', async () => { + const conditionUuid = 'condition-uuid'; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'OTHER' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + + await expect(service.getAll(conditionUuid)).rejects.toThrow(ServiceValidationException); + }); + }); + + describe('getByUuid', () => { + it('should return a financial instrument by uuid', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + + const result = await service.getByUuid(conditionUuid, uuid); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockApplicationDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + }); + + it('should throw an error if financial instrument is not found', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.getByUuid(conditionUuid, uuid)).rejects.toThrow(ServiceNotFoundException); + }); + }); + + describe('create', () => { + it('should create a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.EFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + }; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = mockApplicationDecisionConditionFinancialInstrument; + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.save.mockResolvedValue(financialInstrument); + + const result = await service.create(conditionUuid, dto); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockApplicationDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.save).toHaveBeenCalledWith(expect.any(ApplicationDecisionConditionFinancialInstrument)); + }); + }); + + describe('update', () => { + it('should update a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.EFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + }; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + mockRepository.save.mockResolvedValue(financialInstrument); + + const result = await service.update(conditionUuid, uuid, dto); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockApplicationDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + expect(mockRepository.save).toHaveBeenCalledWith(expect.any(ApplicationDecisionConditionFinancialInstrument)); + }); + }); + + describe('remove', () => { + it('should remove a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + mockRepository.remove.mockResolvedValue(financialInstrument); + + const result = await service.remove(conditionUuid, uuid); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ where: { code: 'BOND' } }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + expect(mockRepository.remove).toHaveBeenCalledWith(financialInstrument); + }); + }); +}); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts new file mode 100644 index 0000000000..5e163ba2d8 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts @@ -0,0 +1,186 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + ApplicationDecisionConditionFinancialInstrument, + InstrumentStatus, + InstrumentType, +} from './application-decision-condition-financial-instrument.entity'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + ServiceInternalErrorException, + ServiceNotFoundException, + ServiceValidationException, +} from '../../../../../../../libs/common/src/exceptions/base.exception'; +import { CreateUpdateApplicationDecisionConditionFinancialInstrumentDto } from './application-decision-condition-financial-instrument.dto'; +import { ApplicationDecisionCondition } from '../application-decision-condition.entity'; +import { ApplicationDecisionConditionType } from '../application-decision-condition-code.entity'; + +export enum ConditionType { + FINANCIAL_SECURITY = 'BOND', +} + +@Injectable() +export class ApplicationDecisionConditionFinancialInstrumentService { + constructor( + @InjectRepository(ApplicationDecisionConditionFinancialInstrument) + private readonly repository: Repository, + @InjectRepository(ApplicationDecisionCondition) + private readonly applicationDecisionConditionRepository: Repository, + @InjectRepository(ApplicationDecisionConditionType) + private readonly applicationDecisionConditionTypeRepository: Repository, + ) {} + + async throwErrorIfFinancialSecurityTypeNotExists(): Promise { + const exists = await this.applicationDecisionConditionTypeRepository.findOne({ + where: { code: ConditionType.FINANCIAL_SECURITY }, + }); + if (!exists) { + throw new ServiceInternalErrorException('Condition type Financial Security not found'); + } + } + + async getAll(conditionUuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + return this.repository.find({ where: { condition: { uuid: conditionUuid } } }); + } + + async getByUuid(conditionUuid: string, uuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + const financialInstrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!financialInstrument) { + throw new ServiceNotFoundException(`Financial Instrument with uuid ${uuid} not found`); + } + + return financialInstrument; + } + + async create( + conditionUuid: string, + dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + let instrument = new ApplicationDecisionConditionFinancialInstrument(); + instrument = this.mapDtoToEntity(dto, instrument); + instrument.condition = condition; + + return this.repository.save(instrument); + } + + async update( + conditionUuid: string, + uuid: string, + dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + let instrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!instrument) { + throw new ServiceNotFoundException(`Instrument with uuid ${uuid} not found`); + } + + instrument = this.mapDtoToEntity(dto, instrument); + + return this.repository.save(instrument); + } + + async remove(conditionUuid: string, uuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + const instrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!instrument) { + throw new ServiceNotFoundException(`Instrument with uuid ${uuid} not found`); + } + + return await this.repository.remove(instrument); + } + + private mapDtoToEntity( + dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + entity: ApplicationDecisionConditionFinancialInstrument, + ): ApplicationDecisionConditionFinancialInstrument { + entity.securityHolderPayee = dto.securityHolderPayee; + entity.type = dto.type; + entity.issueDate = new Date(dto.issueDate); + entity.expiryDate = dto.expiryDate ? new Date(dto.expiryDate) : null; + entity.amount = dto.amount; + entity.bank = dto.bank; + if (dto.type !== InstrumentType.EFT && !dto.instrumentNumber) { + throw new ServiceValidationException('Instrument number is required when type is not EFT'); + } + entity.instrumentNumber = dto.instrumentNumber ?? null; + entity.heldBy = dto.heldBy; + entity.receivedDate = new Date(dto.receivedDate); + entity.notes = dto.notes ?? null; + entity.status = dto.status; + if (dto.status !== InstrumentStatus.RECEIVED) { + if (!dto.statusDate || !dto.explanation) { + throw new ServiceValidationException('Status date and explanation are required when status is not RECEIVED'); + } + entity.statusDate = new Date(dto.statusDate); + entity.explanation = dto.explanation; + } else { + if (dto.statusDate || dto.explanation) { + throw new ServiceValidationException('Status date and explanation are not allowed when status is RECEIVED'); + } + entity.statusDate = null; + entity.explanation = null; + } + return entity; + } +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts index 5229461122..fbf1f72739 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts @@ -9,13 +9,22 @@ import { ApplicationDecisionConditionController } from './application-decision-c import { UpdateApplicationDecisionConditionDto } from './application-decision-condition.dto'; import { ApplicationDecisionCondition } from './application-decision-condition.entity'; import { ApplicationDecisionConditionService } from './application-decision-condition.service'; +import { ApplicationDecisionConditionFinancialInstrumentService } from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service'; +import { + ApplicationDecisionConditionFinancialInstrument, + HeldBy, + InstrumentStatus, + InstrumentType, +} from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; describe('ApplicationDecisionConditionController', () => { let controller: ApplicationDecisionConditionController; let mockApplicationDecisionConditionService: DeepMocked; + let mockApplicationDecisionConditionFinancialInstrumentService: DeepMocked; beforeEach(async () => { mockApplicationDecisionConditionService = createMock(); + mockApplicationDecisionConditionFinancialInstrumentService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -30,6 +39,10 @@ describe('ApplicationDecisionConditionController', () => { provide: ApplicationDecisionConditionService, useValue: mockApplicationDecisionConditionService, }, + { + provide: ApplicationDecisionConditionFinancialInstrumentService, + useValue: mockApplicationDecisionConditionFinancialInstrumentService, + }, { provide: ClsService, useValue: {}, @@ -88,4 +101,134 @@ describe('ApplicationDecisionConditionController', () => { expect(result.approvalDependant).toEqual(updated.approvalDependant); }); }); + + describe('Financial Instruments', () => { + const conditionUuid = 'condition-uuid'; + const instrumentUuid = 'instrument-uuid'; + const financialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + notes: 'notes', + expiryDate: new Date().getTime(), + statusDate: new Date().getTime(), + explanation: 'explanation', + }; + + it('should get all financial instruments for a condition', async () => { + const financialInstruments = [ + new ApplicationDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }), + ]; + mockApplicationDecisionConditionFinancialInstrumentService.getAll.mockResolvedValue(financialInstruments); + + const result = await controller.getAllFinancialInstruments(conditionUuid); + + expect(mockApplicationDecisionConditionFinancialInstrumentService.getAll).toHaveBeenCalledWith(conditionUuid); + expect(result).toBeDefined(); + }); + + it('should get a financial instrument by uuid', async () => { + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockApplicationDecisionConditionFinancialInstrumentService.getByUuid.mockResolvedValue(financialInstrument); + + const result = await controller.getFinancialInstrumentByUuid(conditionUuid, instrumentUuid); + + expect(mockApplicationDecisionConditionFinancialInstrumentService.getByUuid).toHaveBeenCalledWith( + conditionUuid, + instrumentUuid, + ); + expect(result).toBeDefined(); + }); + + it('should create a financial instrument', async () => { + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockApplicationDecisionConditionFinancialInstrumentService.create.mockResolvedValue(financialInstrument); + + const result = await controller.createFinancialInstrument(conditionUuid, financialInstrumentDto); + + expect(mockApplicationDecisionConditionFinancialInstrumentService.create).toHaveBeenCalledWith( + conditionUuid, + financialInstrumentDto, + ); + expect(result).toBeDefined(); + }); + + it('should update a financial instrument', async () => { + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockApplicationDecisionConditionFinancialInstrumentService.update.mockResolvedValue(financialInstrument); + + const result = await controller.updateFinancialInstrument(conditionUuid, instrumentUuid, financialInstrumentDto); + + expect(mockApplicationDecisionConditionFinancialInstrumentService.update).toHaveBeenCalledWith( + conditionUuid, + instrumentUuid, + financialInstrumentDto, + ); + expect(result).toBeDefined(); + }); + + it('should delete a financial instrument', async () => { + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockApplicationDecisionConditionFinancialInstrumentService.remove.mockResolvedValue(financialInstrument); + + const result = await controller.deleteFinancialInstrument(conditionUuid, instrumentUuid); + + expect(mockApplicationDecisionConditionFinancialInstrumentService.remove).toHaveBeenCalledWith( + conditionUuid, + instrumentUuid, + ); + expect(result).toBeDefined(); + }); + }); }); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts index ae312f05ef..11e00be122 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts @@ -1,6 +1,6 @@ import { Mapper } from 'automapper-core'; import { InjectMapper } from 'automapper-nestjs'; -import { Body, Controller, Get, Param, Patch, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; @@ -14,6 +14,12 @@ import { } from './application-decision-condition.dto'; import { ApplicationDecisionCondition } from './application-decision-condition.entity'; import { ApplicationDecisionConditionService } from './application-decision-condition.service'; +import { ApplicationDecisionConditionFinancialInstrumentService } from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service'; +import { + ApplicationDecisionConditionFinancialInstrumentDto, + CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, +} from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto'; +import { ApplicationDecisionConditionFinancialInstrument } from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('application-decision-condition') @@ -21,6 +27,7 @@ import { ApplicationDecisionConditionService } from './application-decision-cond export class ApplicationDecisionConditionController { constructor( private conditionService: ApplicationDecisionConditionService, + private conditionFinancialInstrumentService: ApplicationDecisionConditionFinancialInstrumentService, @InjectMapper() private mapper: Mapper, ) {} @@ -75,4 +82,81 @@ export class ApplicationDecisionConditionController { ApplicationDecisionConditionComponentDto, ); } + + @Get('/:uuid/financial-instruments') + @UserRoles(...ANY_AUTH_ROLE) + async getAllFinancialInstruments( + @Param('uuid') uuid: string, + ): Promise { + const financialInstruments = await this.conditionFinancialInstrumentService.getAll(uuid); + + return await this.mapper.mapArray( + financialInstruments, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ); + } + + @Get('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async getFinancialInstrumentByUuid( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.getByUuid(uuid, instrumentUuid); + return await this.mapper.map( + financialInstrument, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ); + } + + @Post('/:uuid/financial-instruments') + @UserRoles(...ANY_AUTH_ROLE) + async createFinancialInstrument( + @Param('uuid') uuid: string, + @Body() dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.create(uuid, dto); + return await this.mapper.map( + financialInstrument, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ); + } + + @Patch('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async updateFinancialInstrument( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + @Body() dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.update(uuid, instrumentUuid, dto); + return await this.mapper.map( + financialInstrument, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ); + } + + @Delete('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async deleteFinancialInstrument( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + ): Promise { + const result = await this.conditionFinancialInstrumentService.remove(uuid, instrumentUuid); + return await this.mapper.map( + result, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ); + } + + @Post('/sort') + @UserRoles(...ANY_AUTH_ROLE) + async sortConditions(@Body() data: { uuid: string; order: number }[]): Promise { + await this.conditionService.setSorting(data); + } } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts index cb228640a2..80c0889a05 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts @@ -9,6 +9,7 @@ import { ApplicationDecisionConditionCardUuidDto, ApplicationDecisionConditionHomeCardDto, } from './application-decision-condition-card/application-decision-condition-card.dto'; +import { ApplicationDecisionConditionFinancialInstrumentDto } from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto'; import { ApplicationTypeDto } from '../../code/application-code/application-type/application-type.dto'; export class ApplicationDecisionConditionTypeDto extends BaseCodeDto { @@ -98,6 +99,12 @@ export class ApplicationDecisionConditionDto { conditionCard: ApplicationDecisionConditionCardUuidDto | null; status?: string | null; + + @AutoMap(() => ApplicationDecisionConditionFinancialInstrumentDto) + financialInstruments?: ApplicationDecisionConditionFinancialInstrumentDto[] | null; + + @AutoMap() + order: number; } export class ApplicationHomeDto { @@ -184,6 +191,10 @@ export class UpdateApplicationDecisionConditionDto { @IsOptional() @IsUUID() conditionCardUuid?: string; + + @IsOptional() + @IsNumber() + order?: number; } export class UpdateApplicationDecisionConditionServiceDto { diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts index 455e7aca11..20bc915e61 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts @@ -8,6 +8,7 @@ import { ApplicationDecision } from '../application-decision.entity'; import { ApplicationDecisionConditionType } from './application-decision-condition-code.entity'; import { ApplicationDecisionConditionDate } from './application-decision-condition-date/application-decision-condition-date.entity'; import { ApplicationDecisionConditionCard } from './application-decision-condition-card/application-decision-condition-card.entity'; +import { ApplicationDecisionConditionFinancialInstrument } from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; @Entity({ comment: 'Fields present on the application decision conditions' }) export class ApplicationDecisionCondition extends Base { @@ -60,6 +61,10 @@ export class ApplicationDecisionCondition extends Base { @Column() decisionUuid: string; + @AutoMap(() => Number) + @Column({ default: 0 }) + order: number; + @ManyToMany(() => ApplicationDecisionComponent, (component) => component.conditions, { nullable: true }) @JoinTable({ name: 'application_decision_condition_component', @@ -87,4 +92,9 @@ export class ApplicationDecisionCondition extends Base { nullable: true, }) conditionCard: ApplicationDecisionConditionCard | null; + + @OneToMany(() => ApplicationDecisionConditionFinancialInstrument, (instrument) => instrument.condition, { + cascade: true, + }) + financialInstruments?: ApplicationDecisionConditionFinancialInstrument[] | null; } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts index 4d06eabcb1..9356416194 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { FindOptionsWhere, In, IsNull, Repository } from 'typeorm'; +import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Repository } from 'typeorm'; import { ServiceValidationException } from '../../../../../../libs/common/src/exceptions/base.exception'; import { ApplicationDecisionConditionToComponentLot } from '../application-condition-to-component-lot/application-decision-condition-to-component-lot.entity'; import { ApplicationDecisionConditionComponentPlanNumber } from '../application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity'; @@ -32,6 +32,15 @@ export class ApplicationDecisionConditionService { assignee: true, }; + private DEFAULT_APP_RELATIONS: FindOptionsRelations = { + application: { + type: true, + region: true, + localGovernment: true, + decisionMeetings: true, + }, + }; + constructor( @InjectRepository(ApplicationDecisionCondition) private repository: Repository, @@ -109,19 +118,15 @@ export class ApplicationDecisionConditionService { const condition = this.mapper.map(c, ApplicationDecisionCondition, ApplicationDecisionConditionHomeDto); const decision = this.mapper.map(c.decision, ApplicationDecision, ApplicationDecisionHomeDto); const application = this.mapper.map(c.decision.application, Application, ApplicationHomeDto); + const appModifications = await this.modificationRepository.find({ - where: { - modifiesDecisions: { - uuid: c.decision?.uuid, - }, - }, + where: { application: { fileNumber: condition?.decision?.application.fileNumber } }, + relations: this.DEFAULT_APP_RELATIONS, }); + const appReconsiderations = await this.reconsiderationRepository.find({ - where: { - reconsidersDecisions: { - uuid: c.decision?.uuid, - }, - }, + where: { application: { fileNumber: condition?.decision?.application.fileNumber } }, + relations: this.DEFAULT_APP_RELATIONS, }); return { @@ -216,6 +221,7 @@ export class ApplicationDecisionConditionService { condition.description = updateDto.description ?? null; condition.securityAmount = updateDto.securityAmount ?? null; condition.approvalDependant = updateDto.approvalDependant ?? null; + condition.order = updateDto.order ?? 0; if (updateDto.dates) { condition.dates = updateDto.dates.map((dateDto) => { const dateEntity = new ApplicationDecisionConditionDate(); @@ -318,4 +324,22 @@ export class ApplicationDecisionConditionService { await this.conditionComponentPlanNumbersRepository.save(conditionToComponent); } + + async setSorting(data: { uuid: string; order: number }[]) { + const uuids = data.map((data) => data.uuid); + const conditions = await this.repository.find({ + where: { + uuid: In(uuids), + }, + }); + + for (const condition of data) { + const existingCondition = conditions.find((c) => c.uuid === condition.uuid); + if (existingCondition) { + existingCondition.order = condition.order; + } + } + + await this.repository.save(conditions); + } } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts index e06f83986f..6dab2ef843 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts @@ -54,6 +54,8 @@ import { ApplicationDecisionConditionCardController } from '../application-decis import { ApplicationDecisionConditionCard } from '../application-decision-condition/application-decision-condition-card/application-decision-condition-card.entity'; import { ApplicationDecisionConditionCardService } from '../application-decision-condition/application-decision-condition-card/application-decision-condition-card.service'; import { User } from 'apps/alcs/src/user/user.entity'; +import { ApplicationDecisionConditionFinancialInstrument } from '../application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; +import { ApplicationDecisionConditionFinancialInstrumentService } from '../application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service'; @Module({ imports: [ @@ -84,6 +86,7 @@ import { User } from 'apps/alcs/src/user/user.entity'; ApplicationBoundaryAmendment, ApplicationDecisionConditionCard, User, + ApplicationDecisionConditionFinancialInstrument, ]), forwardRef(() => BoardModule), forwardRef(() => ApplicationModule), @@ -105,6 +108,7 @@ import { User } from 'apps/alcs/src/user/user.entity'; ApplicationConditionToComponentLotService, ApplicationBoundaryAmendmentService, ApplicationDecisionConditionCardService, + ApplicationDecisionConditionFinancialInstrumentService, ], controllers: [ ApplicationDecisionV2Controller, diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts index 48c1babb94..152f1ee417 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts @@ -126,6 +126,7 @@ export class ApplicationDecisionV2Service { }, dates: true, conditionCard: true, + financialInstruments: true, }, conditionCards: true, }, diff --git a/services/apps/alcs/src/alcs/code/application-code/application-code.dto.ts b/services/apps/alcs/src/alcs/code/application-code/application-code.dto.ts index 8d9e015020..cec63d0c9c 100644 --- a/services/apps/alcs/src/alcs/code/application-code/application-code.dto.ts +++ b/services/apps/alcs/src/alcs/code/application-code/application-code.dto.ts @@ -3,6 +3,7 @@ import { ReconsiderationTypeDto } from '../../application-decision/application-r import { CardStatusDto } from '../../card/card-status/card-status.dto'; import { ApplicationRegionDto } from './application-region/application-region.dto'; import { ApplicationTypeDto } from './application-type/application-type.dto'; +import { ApplicationDecisionMakerCodeDto } from '../../application-decision/application-decision-maker/decision-maker.dto'; export class MasterCodesDto { type: ApplicationTypeDto[]; @@ -10,4 +11,5 @@ export class MasterCodesDto { region: ApplicationRegionDto[]; reconsiderationType: ReconsiderationTypeDto[]; applicationStatusType: ApplicationStatusDto[]; + decisionMaker: ApplicationDecisionMakerCodeDto[]; } diff --git a/services/apps/alcs/src/alcs/code/code.controller.ts b/services/apps/alcs/src/alcs/code/code.controller.ts index 01b68675b5..36b7f220e6 100644 --- a/services/apps/alcs/src/alcs/code/code.controller.ts +++ b/services/apps/alcs/src/alcs/code/code.controller.ts @@ -18,6 +18,8 @@ import { ApplicationRegion } from './application-code/application-region/applica import { ApplicationTypeDto } from './application-code/application-type/application-type.dto'; import { ApplicationType } from './application-code/application-type/application-type.entity'; import { CodeService } from './code.service'; +import { ApplicationDecisionMakerCode } from '../application-decision/application-decision-maker/application-decision-maker.entity'; +import { ApplicationDecisionMakerCodeDto } from '../application-decision/application-decision-maker/decision-maker.dto'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('code') @@ -35,16 +37,8 @@ export class CodeController { return { status: this.mapper.mapArray(types.status, CardStatus, CardStatusDto), - type: this.mapper.mapArray( - types.type, - ApplicationType, - ApplicationTypeDto, - ), - region: this.mapper.mapArray( - types.region, - ApplicationRegion, - ApplicationRegionDto, - ), + type: this.mapper.mapArray(types.type, ApplicationType, ApplicationTypeDto), + region: this.mapper.mapArray(types.region, ApplicationRegion, ApplicationRegionDto), reconsiderationType: this.mapper.mapArray( types.reconsiderationTypes, ApplicationReconsiderationType, @@ -55,6 +49,11 @@ export class CodeController { ApplicationSubmissionStatusType, ApplicationStatusDto, ), + decisionMaker: this.mapper.mapArray( + types.decisionMakers, + ApplicationDecisionMakerCode, + ApplicationDecisionMakerCodeDto, + ), }; } } diff --git a/services/apps/alcs/src/alcs/home/home.controller.ts b/services/apps/alcs/src/alcs/home/home.controller.ts index cf8e141240..18c40eaf3a 100644 --- a/services/apps/alcs/src/alcs/home/home.controller.ts +++ b/services/apps/alcs/src/alcs/home/home.controller.ts @@ -3,7 +3,7 @@ import { ApiOAuth2 } from '@nestjs/swagger'; import { Mapper } from 'automapper-core'; import { InjectMapper } from 'automapper-nestjs'; import * as config from 'config'; -import { In, Not, Repository } from 'typeorm'; +import { FindOptionsRelations, In, Not, Repository } from 'typeorm'; import { ANY_AUTH_ROLE } from '../../common/authorization/roles'; import { RolesGuard } from '../../common/authorization/roles-guard.service'; import { UserRoles } from '../../common/authorization/roles.decorator'; @@ -49,6 +49,22 @@ import { InjectRepository } from '@nestjs/typeorm'; const HIDDEN_CARD_STATUSES = [CARD_STATUS.CANCELLED, CARD_STATUS.DECISION_RELEASED]; +const DEFAULT_APP_RELATIONS: FindOptionsRelations = { + application: { + type: true, + region: true, + localGovernment: true, + decisionMeetings: true, + }, +}; + +const DEFAULT_NOI_RELATIONS: FindOptionsRelations = { + noticeOfIntent: { + region: true, + localGovernment: true, + }, +}; + @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('home') @UseGuards(RolesGuard) @@ -308,20 +324,17 @@ export class HomeController { if (!condition.conditionCard?.card) { continue; } + const appModifications = await this.modificationApplicationRepository.find({ - where: { - modifiesDecisions: { - uuid: condition.decision?.uuid, - }, - }, + where: { application: { fileNumber: condition.decision.application.fileNumber } }, + relations: DEFAULT_APP_RELATIONS, }); + const appReconsiderations = await this.reconsiderationApplicationRepository.find({ - where: { - reconsidersDecisions: { - uuid: condition.decision?.uuid, - }, - }, + where: { application: { fileNumber: condition.decision.application.fileNumber } }, + relations: DEFAULT_APP_RELATIONS, }); + for (const subtask of condition.conditionCard?.card?.subtasks) { result.push({ isCondition: true, @@ -349,9 +362,6 @@ export class HomeController { } private async mapNoticeOfIntentConditionsToDtos(noticeOfIntestConditions: NoticeOfIntentDecisionCondition[]) { - const noticeOfIntents = noticeOfIntestConditions.map((c) => c.decision.noticeOfIntent); - const uuids = noticeOfIntents.map((noi) => noi.uuid); - const timeMap = await this.noticeOfIntentService.getTimes(uuids); const holidays = await this.holidayService.fetchAllHolidays(); const result: HomepageSubtaskDTO[] = []; @@ -371,11 +381,8 @@ export class HomeController { continue; } const noiModifications = await this.modificationNoticeOfIntentRepository.find({ - where: { - modifiesDecisions: { - uuid: condition.decision?.uuid, - }, - }, + where: { noticeOfIntent: { fileNumber: condition.decision.noticeOfIntent.fileNumber } }, + relations: DEFAULT_NOI_RELATIONS, }); for (const subtask of condition.conditionCard?.card?.subtasks) { result.push({ diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.spec.ts index 131f2f970e..3e03ffc360 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.spec.ts @@ -116,7 +116,7 @@ describe('NoticeOfIntentDecisionConditionCardController', () => { conditionCard.decision = { uuid: 'decision-uuid', noticeOfIntent: { fileNumber: 'file-number' } } as any; mockService.getByBoardCard.mockResolvedValue(conditionCard); - mockModificationService.getByNoticeOfIntentDecisionUuid.mockResolvedValue([]); + mockModificationService.getByFileNumber.mockResolvedValue([]); mockDecisionService.getDecisionOrder.mockResolvedValue(1); const result = await controller.getByCardUuid(uuid); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.ts index bb3ac2b147..1ca8fd662b 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.ts @@ -68,9 +68,7 @@ export class NoticeOfIntentDecisionConditionCardController { ); dto.fileNumber = result.decision.noticeOfIntent.fileNumber; - const appModifications = await this.noticeOfIntentModificationService.getByNoticeOfIntentDecisionUuid( - result.decision.uuid, - ); + const appModifications = await this.noticeOfIntentModificationService.getByFileNumber(dto.fileNumber); dto.isModification = appModifications.length > 0; diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service.ts index 66db9db75f..a8f4133c09 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service.ts @@ -194,9 +194,7 @@ export class NoticeOfIntentDecisionConditionCardService { }); for (const dto of dtos) { - const appModifications = await this.noticeOfIntentModificationService.getByNoticeOfIntentDecisionUuid( - dto.decisionUuid, - ); + const appModifications = await this.noticeOfIntentModificationService.getByFileNumber(dto.fileNumber); dto.isModification = appModifications.length > 0; diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts new file mode 100644 index 0000000000..1678e851f9 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts @@ -0,0 +1,75 @@ +import { AutoMap } from 'automapper-classes'; +import { + InstrumentType, + HeldBy, + InstrumentStatus, +} from './notice-of-intent-decision-condition-financial-instrument.entity'; +import { IsEnum, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; +import { OmitType } from '@nestjs/mapped-types'; + +export class NoticeOfIntentDecisionConditionFinancialInstrumentDto { + @AutoMap() + @IsUUID() + uuid: string; + + @AutoMap() + @IsString() + securityHolderPayee: string; + + @AutoMap() + @IsEnum(InstrumentType) + type: InstrumentType; + + @AutoMap() + @IsNumber() + issueDate: number; + + @AutoMap() + @IsNumber() + @IsOptional() + expiryDate?: number | null; + + @AutoMap() + @IsNumber() + amount: number; + + @AutoMap() + @IsString() + bank: string; + + @AutoMap() + @IsOptional() + instrumentNumber?: string | null; + + @AutoMap() + @IsEnum(HeldBy) + heldBy: HeldBy; + + @AutoMap() + @IsNumber() + receivedDate: number; + + @AutoMap() + @IsString() + @IsOptional() + notes?: string | null; + + @AutoMap() + @IsEnum(InstrumentStatus) + status: InstrumentStatus; + + @AutoMap() + @IsOptional() + @IsNumber() + statusDate?: number | null; + + @AutoMap() + @IsString() + @IsOptional() + explanation?: string | null; +} + +export class CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto extends OmitType( + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ['uuid'] as const, +) {} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity.ts new file mode 100644 index 0000000000..1cf40257ba --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity.ts @@ -0,0 +1,99 @@ +import { Entity, Column, ManyToOne } from 'typeorm'; +import { Base } from '../../../../common/entities/base.entity'; +import { AutoMap } from 'automapper-classes'; +import { ColumnNumericTransformer } from '../../../../utils/column-numeric-transform'; +import { NoticeOfIntentDecisionCondition } from '../notice-of-intent-decision-condition.entity'; + +export enum InstrumentType { + BANK_DRAFT = 'Bank Draft', + CERTIFIED_CHEQUE = 'Certified Cheque', + EFT = 'EFT', + IRREVOCABLE_LETTER_OF_CREDIT = 'Irrevocable Letter of Credit', + OTHER = 'Other', + SAFEKEEPING_AGREEMENT = 'Safekeeping Agreement', +} + +export enum HeldBy { + ALC = 'ALC', + MINISTRY = 'Ministry', +} + +export enum InstrumentStatus { + RECEIVED = 'Received', + RELEASED = 'Released', + CASHED = 'Cashed', + REPLACED = 'Replaced', +} + +@Entity({ comment: 'Instrument for Financial Security Conditions' }) +export class NoticeOfIntentDecisionConditionFinancialInstrument extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @Column({ type: 'varchar', nullable: false }) + securityHolderPayee: string; + + @AutoMap() + @Column({ type: 'enum', enum: InstrumentType, nullable: false, enumName: 'noi_instrument_type' }) + type: InstrumentType; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: false }) + issueDate: Date; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: true }) + expiryDate?: Date | null; + + @AutoMap() + @Column({ type: 'decimal', precision: 12, scale: 2, nullable: false, transformer: new ColumnNumericTransformer() }) + amount: number; + + @AutoMap() + @Column({ type: 'varchar', nullable: false }) + bank: string; + + @AutoMap() + @Column({ type: 'varchar', nullable: true }) + instrumentNumber: string | null; + + @AutoMap() + @Column({ type: 'enum', enum: HeldBy, nullable: false, enumName: 'noi_instrument_held_by' }) + heldBy: HeldBy; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: false }) + receivedDate: Date; + + @AutoMap() + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @AutoMap() + @Column({ + type: 'enum', + enum: InstrumentStatus, + default: InstrumentStatus.RECEIVED, + nullable: false, + enumName: 'noi_instrument_status', + }) + status: InstrumentStatus; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: true }) + statusDate?: Date | null; + + @AutoMap() + @Column({ type: 'text', nullable: true }) + explanation?: string | null; + + @ManyToOne(() => NoticeOfIntentDecisionCondition, (condition) => condition.financialInstruments, { + onDelete: 'CASCADE', + }) + condition: NoticeOfIntentDecisionCondition; +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts new file mode 100644 index 0000000000..2ca488f99b --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts @@ -0,0 +1,289 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from './notice-of-intent-decision-condition-financial-instrument.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { + NoticeOfIntentDecisionConditionFinancialInstrument, + HeldBy, + InstrumentStatus, + InstrumentType, +} from './notice-of-intent-decision-condition-financial-instrument.entity'; +import { NoticeOfIntentDecisionCondition } from '../notice-of-intent-decision-condition.entity'; +import { NoticeOfIntentDecisionConditionType } from '../notice-of-intent-decision-condition-code.entity'; +import { Repository } from 'typeorm'; +import { CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto } from './notice-of-intent-decision-condition-financial-instrument.dto'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { + ServiceInternalErrorException, + ServiceNotFoundException, + ServiceValidationException, +} from '../../../../../../../libs/common/src/exceptions/base.exception'; +import { + initNoticeOfIntentDecisionConditionFinancialInstrumentMockEntity, + initNoticeOfIntentDecisionConditionTypeMockEntity, +} from '../../../../../test/mocks/mockEntities'; + +describe('NoticeOfIntentDecisionConditionFinancialInstrumentService', () => { + let service: NoticeOfIntentDecisionConditionFinancialInstrumentService; + let mockRepository: DeepMocked>; + let mockConditionRepository: DeepMocked>; + let mockConditionTypeRepository: DeepMocked>; + let mockNoticeOfIntentDecisionConditionType; + let mockNoticeOfIntentDecisionConditionFinancialInstrument; + + beforeEach(async () => { + mockRepository = createMock(); + mockConditionRepository = createMock(); + mockConditionTypeRepository = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeOfIntentDecisionConditionFinancialInstrumentService, + { + provide: getRepositoryToken(NoticeOfIntentDecisionConditionFinancialInstrument), + useValue: mockRepository, + }, + { + provide: getRepositoryToken(NoticeOfIntentDecisionCondition), + useValue: mockConditionRepository, + }, + { + provide: getRepositoryToken(NoticeOfIntentDecisionConditionType), + useValue: mockConditionTypeRepository, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentDecisionConditionFinancialInstrumentService, + ); + + mockNoticeOfIntentDecisionConditionType = initNoticeOfIntentDecisionConditionTypeMockEntity('BOND'); + mockNoticeOfIntentDecisionConditionFinancialInstrument = + initNoticeOfIntentDecisionConditionFinancialInstrumentMockEntity(); + }); + + describe('getAll', () => { + it('should return all financial instruments for a condition', async () => { + const conditionUuid = 'condition-uuid'; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstruments = [mockNoticeOfIntentDecisionConditionFinancialInstrument]; + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.find.mockResolvedValue(financialInstruments); + + const result = await service.getAll(conditionUuid); + + expect(result).toEqual(financialInstruments); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ where: { code: 'BOND' } }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.find).toHaveBeenCalledWith({ where: { condition: { uuid: conditionUuid } } }); + }); + + it('should throw an error if condition type does not exist', async () => { + mockConditionTypeRepository.findOne.mockResolvedValue(null); + + await expect(service.getAll('condition-uuid')).rejects.toThrow(ServiceInternalErrorException); + }); + + it('should throw an error if condition is not found', async () => { + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(null); + + await expect(service.getAll('condition-uuid')).rejects.toThrow(ServiceNotFoundException); + }); + + it('should throw an error if condition is not of type Financial Security', async () => { + const conditionUuid = 'condition-uuid'; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'OTHER' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + + await expect(service.getAll(conditionUuid)).rejects.toThrow(ServiceValidationException); + }); + }); + + describe('getByUuid', () => { + it('should return a financial instrument by uuid', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + + const result = await service.getByUuid(conditionUuid, uuid); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockNoticeOfIntentDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + }); + + it('should throw an error if financial instrument is not found', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.getByUuid(conditionUuid, uuid)).rejects.toThrow(ServiceNotFoundException); + }); + }); + + describe('create', () => { + it('should create a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.EFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + }; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = mockNoticeOfIntentDecisionConditionFinancialInstrument; + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.save.mockResolvedValue(financialInstrument); + + const result = await service.create(conditionUuid, dto); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockNoticeOfIntentDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.save).toHaveBeenCalledWith(expect.any(NoticeOfIntentDecisionConditionFinancialInstrument)); + }); + }); + + describe('update', () => { + it('should update a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.EFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + }; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + mockRepository.save.mockResolvedValue(financialInstrument); + + const result = await service.update(conditionUuid, uuid, dto); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockNoticeOfIntentDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + expect(mockRepository.save).toHaveBeenCalledWith(expect.any(NoticeOfIntentDecisionConditionFinancialInstrument)); + }); + }); + + describe('remove', () => { + it('should remove a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + mockRepository.remove.mockResolvedValue(financialInstrument); + + const result = await service.remove(conditionUuid, uuid); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ where: { code: 'BOND' } }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + expect(mockRepository.remove).toHaveBeenCalledWith(financialInstrument); + }); + }); + + it('should throw an error if instrument number is missing when type is not EFT', async () => { + const conditionUuid = 'condition-uuid'; + const dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + }; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + + await expect(service.create(conditionUuid, dto)).rejects.toThrow(ServiceValidationException); + }); + + it('should throw an error if status date or explanation is missing when status is not RECEIVED', async () => { + const conditionUuid = 'condition-uuid'; + const dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.EFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.CASHED, + }; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + + await expect(service.create(conditionUuid, dto)).rejects.toThrow(ServiceValidationException); + }); + + it('should throw an error if status date or explanation is provided when status is RECEIVED', async () => { + const conditionUuid = 'condition-uuid'; + const dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.EFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + explanation: 'test', + statusDate: new Date().getTime(), + }; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + + await expect(service.create(conditionUuid, dto)).rejects.toThrow(ServiceValidationException); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts new file mode 100644 index 0000000000..f6cd69f3ba --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts @@ -0,0 +1,186 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + NoticeOfIntentDecisionConditionFinancialInstrument, + InstrumentStatus, + InstrumentType, +} from './notice-of-intent-decision-condition-financial-instrument.entity'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + ServiceInternalErrorException, + ServiceNotFoundException, + ServiceValidationException, +} from '../../../../../../../libs/common/src/exceptions/base.exception'; +import { CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto } from './notice-of-intent-decision-condition-financial-instrument.dto'; +import { NoticeOfIntentDecisionCondition } from '../notice-of-intent-decision-condition.entity'; +import { NoticeOfIntentDecisionConditionType } from '../notice-of-intent-decision-condition-code.entity'; + +export enum ConditionType { + FINANCIAL_SECURITY = 'BOND', +} + +@Injectable() +export class NoticeOfIntentDecisionConditionFinancialInstrumentService { + constructor( + @InjectRepository(NoticeOfIntentDecisionConditionFinancialInstrument) + private readonly repository: Repository, + @InjectRepository(NoticeOfIntentDecisionCondition) + private readonly noticeOfIntentDecisionConditionRepository: Repository, + @InjectRepository(NoticeOfIntentDecisionConditionType) + private readonly noticeOfIntentDecisionConditionTypeRepository: Repository, + ) {} + + async throwErrorIfFinancialSecurityTypeNotExists(): Promise { + const exists = await this.noticeOfIntentDecisionConditionTypeRepository.findOne({ + where: { code: ConditionType.FINANCIAL_SECURITY }, + }); + if (!exists) { + throw new ServiceInternalErrorException('Condition type Financial Security not found'); + } + } + + async getAll(conditionUuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.noticeOfIntentDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + return this.repository.find({ where: { condition: { uuid: conditionUuid } } }); + } + + async getByUuid(conditionUuid: string, uuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.noticeOfIntentDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + const financialInstrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!financialInstrument) { + throw new ServiceNotFoundException(`Financial Instrument with uuid ${uuid} not found`); + } + + return financialInstrument; + } + + async create( + conditionUuid: string, + dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.noticeOfIntentDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + let instrument = new NoticeOfIntentDecisionConditionFinancialInstrument(); + instrument = this.mapDtoToEntity(dto, instrument); + instrument.condition = condition; + + return this.repository.save(instrument); + } + + async update( + conditionUuid: string, + uuid: string, + dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.noticeOfIntentDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + let instrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!instrument) { + throw new ServiceNotFoundException(`Instrument with uuid ${uuid} not found`); + } + + instrument = this.mapDtoToEntity(dto, instrument); + + return this.repository.save(instrument); + } + + async remove(conditionUuid: string, uuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.noticeOfIntentDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + const instrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!instrument) { + throw new ServiceNotFoundException(`Instrument with uuid ${uuid} not found`); + } + + return await this.repository.remove(instrument); + } + + private mapDtoToEntity( + dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + entity: NoticeOfIntentDecisionConditionFinancialInstrument, + ): NoticeOfIntentDecisionConditionFinancialInstrument { + entity.securityHolderPayee = dto.securityHolderPayee; + entity.type = dto.type; + entity.issueDate = new Date(dto.issueDate); + entity.expiryDate = dto.expiryDate ? new Date(dto.expiryDate) : null; + entity.amount = dto.amount; + entity.bank = dto.bank; + if (dto.type !== InstrumentType.EFT && !dto.instrumentNumber) { + throw new ServiceValidationException('Instrument number is required when type is not EFT'); + } + entity.instrumentNumber = dto.instrumentNumber ?? null; + entity.heldBy = dto.heldBy; + entity.receivedDate = new Date(dto.receivedDate); + entity.notes = dto.notes ?? null; + entity.status = dto.status; + if (dto.status !== InstrumentStatus.RECEIVED) { + if (!dto.statusDate || !dto.explanation) { + throw new ServiceValidationException('Status date and explanation are required when status is not RECEIVED'); + } + entity.statusDate = new Date(dto.statusDate); + entity.explanation = dto.explanation; + } else { + if (dto.statusDate || dto.explanation) { + throw new ServiceValidationException('Status date and explanation are not allowed when status is RECEIVED'); + } + entity.statusDate = null; + entity.explanation = null; + } + return entity; + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts index 7b5213ef36..0c489f342b 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts @@ -1,5 +1,5 @@ import { classes } from 'automapper-classes'; -import { AutomapperModule } from 'automapper-nestjs'; +import { AutomapperModule, InjectMapper } from 'automapper-nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; @@ -9,13 +9,23 @@ import { NoticeOfIntentDecisionConditionController } from './notice-of-intent-de import { UpdateNoticeOfIntentDecisionConditionDto } from './notice-of-intent-decision-condition.dto'; import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition.entity'; import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition.service'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service'; +import { + NoticeOfIntentDecisionConditionFinancialInstrument, + HeldBy, + InstrumentStatus, + InstrumentType, +} from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; +import { CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto'; describe('NoticeOfIntentDecisionConditionController', () => { let controller: NoticeOfIntentDecisionConditionController; let mockNOIDecisionConditionService: DeepMocked; + let mockFinancialInstrumentService: DeepMocked; beforeEach(async () => { mockNOIDecisionConditionService = createMock(); + mockFinancialInstrumentService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -30,6 +40,10 @@ describe('NoticeOfIntentDecisionConditionController', () => { provide: NoticeOfIntentDecisionConditionService, useValue: mockNOIDecisionConditionService, }, + { + provide: NoticeOfIntentDecisionConditionFinancialInstrumentService, + useValue: mockFinancialInstrumentService, + }, { provide: ClsService, useValue: {}, @@ -88,4 +102,125 @@ describe('NoticeOfIntentDecisionConditionController', () => { expect(result.approvalDependant).toEqual(updated.approvalDependant); }); }); + + describe('Financial Instruments', () => { + const conditionUuid = 'condition-uuid'; + const instrumentUuid = 'instrument-uuid'; + const financialInstrumentDto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + notes: 'notes', + expiryDate: new Date().getTime(), + statusDate: new Date().getTime(), + explanation: 'explanation', + }; + + it('should get all financial instruments for a condition', async () => { + const financialInstruments = [ + new NoticeOfIntentDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }), + ]; + mockFinancialInstrumentService.getAll.mockResolvedValue(financialInstruments); + + const result = await controller.getAllFinancialInstruments(conditionUuid); + + expect(mockFinancialInstrumentService.getAll).toHaveBeenCalledWith(conditionUuid); + expect(result).toBeDefined(); + }); + + it('should get a financial instrument by uuid', async () => { + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockFinancialInstrumentService.getByUuid.mockResolvedValue(financialInstrument); + + const result = await controller.getFinancialInstrumentByUuid(conditionUuid, instrumentUuid); + + expect(mockFinancialInstrumentService.getByUuid).toHaveBeenCalledWith(conditionUuid, instrumentUuid); + expect(result).toBeDefined(); + }); + + it('should create a financial instrument', async () => { + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockFinancialInstrumentService.create.mockResolvedValue(financialInstrument); + + const result = await controller.createFinancialInstrument(conditionUuid, financialInstrumentDto); + + expect(mockFinancialInstrumentService.create).toHaveBeenCalledWith(conditionUuid, financialInstrumentDto); + expect(result).toBeDefined(); + }); + + it('should update a financial instrument', async () => { + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockFinancialInstrumentService.update.mockResolvedValue(financialInstrument); + + const result = await controller.updateFinancialInstrument(conditionUuid, instrumentUuid, financialInstrumentDto); + + expect(mockFinancialInstrumentService.update).toHaveBeenCalledWith( + conditionUuid, + instrumentUuid, + financialInstrumentDto, + ); + expect(result).toBeDefined(); + }); + + it('should delete a financial instrument', async () => { + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockFinancialInstrumentService.remove.mockResolvedValue(financialInstrument); + + const result = await controller.deleteFinancialInstrument(conditionUuid, instrumentUuid); + + expect(mockFinancialInstrumentService.remove).toHaveBeenCalledWith(conditionUuid, instrumentUuid); + expect(result).toBeDefined(); + }); + }); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts index fa2b4be1c5..6ba37bda60 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts @@ -1,6 +1,6 @@ import { Mapper } from 'automapper-core'; import { InjectMapper } from 'automapper-nestjs'; -import { Body, Controller, Get, Param, Patch, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Patch, Query, UseGuards, Post, Delete } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; @@ -12,6 +12,12 @@ import { } from './notice-of-intent-decision-condition.dto'; import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition.entity'; import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition.service'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service'; +import { + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, +} from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto'; +import { NoticeOfIntentDecisionConditionFinancialInstrument } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('notice-of-intent-decision-condition') @@ -19,6 +25,7 @@ import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decis export class NoticeOfIntentDecisionConditionController { constructor( private conditionService: NoticeOfIntentDecisionConditionService, + private conditionFinancialInstrumentService: NoticeOfIntentDecisionConditionFinancialInstrumentService, @InjectMapper() private mapper: Mapper, ) {} @@ -45,4 +52,75 @@ export class NoticeOfIntentDecisionConditionController { NoticeOfIntentDecisionConditionDto, ); } + + @Get('/:uuid/financial-instruments') + @UserRoles(...ANY_AUTH_ROLE) + async getAllFinancialInstruments( + @Param('uuid') uuid: string, + ): Promise { + const financialInstruments = await this.conditionFinancialInstrumentService.getAll(uuid); + + return await this.mapper.mapArray( + financialInstruments, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ); + } + + @Get('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async getFinancialInstrumentByUuid( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.getByUuid(uuid, instrumentUuid); + return await this.mapper.map( + financialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ); + } + + @Post('/:uuid/financial-instruments') + @UserRoles(...ANY_AUTH_ROLE) + async createFinancialInstrument( + @Param('uuid') uuid: string, + @Body() dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.create(uuid, dto); + return await this.mapper.map( + financialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ); + } + + @Patch('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async updateFinancialInstrument( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + @Body() dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.update(uuid, instrumentUuid, dto); + return await this.mapper.map( + financialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ); + } + + @Delete('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async deleteFinancialInstrument( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + ): Promise { + const result = await this.conditionFinancialInstrumentService.remove(uuid, instrumentUuid); + return await this.mapper.map( + result, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ); + } } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts index 30399bbcbf..5f601a7a1d 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts @@ -13,6 +13,7 @@ import { NoticeOfIntentDecisionConditionHomeCardDto, } from './notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.dto'; import { NoticeOfIntentTypeDto } from '../../notice-of-intent/notice-of-intent-type/notice-of-intent-type.dto'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentDto } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto'; export class NoticeOfIntentDecisionConditionTypeDto extends BaseCodeDto { @IsBoolean() @@ -101,6 +102,12 @@ export class NoticeOfIntentDecisionConditionDto { conditionCard: NoticeOfIntentDecisionConditionCardUuidDto | null; status?: string | null; + + @AutoMap(() => NoticeOfIntentDecisionConditionFinancialInstrumentDto) + financialInstruments?: NoticeOfIntentDecisionConditionFinancialInstrumentDto[] | null; + + @AutoMap() + order: number; } export class NoticeOfIntentHomeDto { @@ -116,7 +123,7 @@ export class NoticeOfIntentHomeDto { @AutoMap(() => NoticeOfIntentTypeDto) type: NoticeOfIntentTypeDto; - activeDays: number; + activeDays?: number; paused: boolean; pausedDays: number; } @@ -126,7 +133,7 @@ export class NoticeOfIntentDecisionHomeDto { uuid: string; @AutoMap() - application: NoticeOfIntentHomeDto; + noticeOfIntent: NoticeOfIntentHomeDto; } export class NoticeOfIntentDecisionConditionHomeDto { diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts index 061941e0f2..d0c079072c 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts @@ -7,6 +7,7 @@ import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; import { NoticeOfIntentDecisionConditionType } from './notice-of-intent-decision-condition-code.entity'; import { NoticeOfIntentDecisionConditionDate } from './notice-of-intent-decision-condition-date/notice-of-intent-decision-condition-date.entity'; import { NoticeOfIntentDecisionConditionCard } from './notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.entity'; +import { NoticeOfIntentDecisionConditionFinancialInstrument } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; @Entity({ comment: 'Decision Conditions for Notice of Intents', @@ -61,6 +62,10 @@ export class NoticeOfIntentDecisionCondition extends Base { @Column() decisionUuid: string; + @AutoMap(() => Number) + @Column({ default: 0 }) + order: number; + @ManyToMany(() => NoticeOfIntentDecisionComponent, (component) => component.conditions, { nullable: true }) @JoinTable({ name: 'notice_of_intent_decision_condition_component', @@ -85,4 +90,9 @@ export class NoticeOfIntentDecisionCondition extends Base { nullable: true, }) conditionCard: NoticeOfIntentDecisionConditionCard | null; + + @OneToMany(() => NoticeOfIntentDecisionConditionFinancialInstrument, (instrument) => instrument.condition, { + cascade: true, + }) + financialInstruments?: NoticeOfIntentDecisionConditionFinancialInstrument[] | null; } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts index 80fdbe900d..df55709209 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { FindOptionsWhere, In, IsNull, Repository } from 'typeorm'; +import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Repository } from 'typeorm'; import { ServiceValidationException } from '../../../../../../libs/common/src/exceptions/base.exception'; import { NoticeOfIntentDecisionComponent } from '../notice-of-intent-decision-component/notice-of-intent-decision-component.entity'; import { NoticeOfIntentDecisionConditionType } from './notice-of-intent-decision-condition-code.entity'; @@ -29,6 +29,14 @@ export class NoticeOfIntentDecisionConditionService { assignee: true, }; + private DEFAULT_NOI_RELATIONS: FindOptionsRelations = { + noticeOfIntent: { + type: true, + region: true, + localGovernment: true, + }, + }; + constructor( @InjectRepository(NoticeOfIntentDecisionCondition) private repository: Repository, @@ -132,11 +140,8 @@ export class NoticeOfIntentDecisionConditionService { const decision = this.mapper.map(c.decision, NoticeOfIntentDecision, NoticeOfIntentDecisionHomeDto); const noticeOfIntent = this.mapper.map(c.decision.noticeOfIntent, NoticeOfIntent, NoticeOfIntentHomeDto); const appModifications = await this.modificationRepository.find({ - where: { - modifiesDecisions: { - uuid: c.decision?.uuid, - }, - }, + where: { noticeOfIntent: { fileNumber: condition?.decision?.noticeOfIntent.fileNumber } }, + relations: this.DEFAULT_NOI_RELATIONS, }); return { @@ -147,7 +152,7 @@ export class NoticeOfIntentDecisionConditionService { noticeOfIntent: { ...noticeOfIntent, activeDays: undefined, - pausedDays: timeMap.get(noticeOfIntent.uuid)?.pausedDays ?? null, + pausedDays: timeMap.get(noticeOfIntent.uuid)!.pausedDays || 0, paused: timeMap.get(noticeOfIntent.uuid)?.pausedDays !== null, }, }, diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts index fb11643fc8..a155a3d88f 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts @@ -99,6 +99,7 @@ export class NoticeOfIntentDecisionV2Service { components: true, dates: true, conditionCard: true, + financialInstruments: true, }, conditionCards: true, }, diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts index 8c0e79d1ab..facc7a2754 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts @@ -31,6 +31,8 @@ import { User } from '../../user/user.entity'; import { NoticeOfIntentDecisionConditionCard } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.entity'; import { NoticeOfIntentDecisionConditionCardService } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service'; import { NoticeOfIntentDecisionConditionCardController } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller'; +import { NoticeOfIntentDecisionConditionFinancialInstrument } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service'; @Module({ imports: [ @@ -46,6 +48,7 @@ import { NoticeOfIntentDecisionConditionCardController } from './notice-of-inten NoticeOfIntentDecisionConditionType, NoticeOfIntentDecisionConditionDate, NoticeOfIntentDecisionConditionCard, + NoticeOfIntentDecisionConditionFinancialInstrument, User, ]), forwardRef(() => BoardModule), @@ -63,6 +66,7 @@ import { NoticeOfIntentDecisionConditionCardController } from './notice-of-inten NoticeOfIntentDecisionProfile, NoticeOfIntentModificationService, NoticeOfIntentDecisionConditionCardService, + NoticeOfIntentDecisionConditionFinancialInstrumentService, ], controllers: [ NoticeOfIntentDecisionV2Controller, diff --git a/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts index 80a08ebe80..d53f6a60a7 100644 --- a/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts +++ b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts @@ -138,6 +138,16 @@ export class ApplicationAdvancedSearchService { promises.push(promise); } + if (searchDto.decisionMaker) { + const promise = APP_SEARCH_FILTERS.addDecisionMakerResults(searchDto, this.applicationRepository); + promises.push(promise); + } + + if (searchDto.decisionOutcomes && searchDto.decisionOutcomes.length > 0) { + const promise = APP_SEARCH_FILTERS.addDecisionOutcomeResults(searchDto, this.applicationRepository); + promises.push(promise); + } + if (searchDto.governmentName) { const promise = APP_SEARCH_FILTERS.addGovernmentResults( searchDto, diff --git a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts index 1677d33c14..96711ff1cb 100644 --- a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts +++ b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts @@ -139,6 +139,11 @@ export class NoticeOfIntentAdvancedSearchService { promises.push(promise); } + if (searchDto.decisionOutcomes && searchDto.decisionOutcomes.length > 0) { + const promise = NOI_SEARCH_FILTERS.addDecisionOutcomeResults(searchDto, this.noiRepository); + promises.push(promise); + } + if (searchDto.governmentName) { const promise = NOI_SEARCH_FILTERS.addGovernmentResults(searchDto, this.noiRepository, this.governmentRepository); promises.push(promise); diff --git a/services/apps/alcs/src/alcs/search/search.dto.ts b/services/apps/alcs/src/alcs/search/search.dto.ts index 401dcf8ac6..b031cb7391 100644 --- a/services/apps/alcs/src/alcs/search/search.dto.ts +++ b/services/apps/alcs/src/alcs/search/search.dto.ts @@ -181,4 +181,11 @@ export class SearchRequestDto extends PagingRequestDto { @IsString() @IsOptional() tagCategoryId?: string; + + @IsArray() + decisionOutcomes?: string[]; + + @IsString() + @IsOptional() + decisionMaker?: string; } diff --git a/services/apps/alcs/src/common/authorization/authorization.service.spec.ts b/services/apps/alcs/src/common/authorization/authorization.service.spec.ts index 331db5ef25..c58633332b 100644 --- a/services/apps/alcs/src/common/authorization/authorization.service.spec.ts +++ b/services/apps/alcs/src/common/authorization/authorization.service.spec.ts @@ -49,6 +49,7 @@ describe('AuthorizationService', () => { clientRoles: [], bceidGuid: '', displayName: '', + email: 'test@example.com', identityProvider: 'idir', uuid: 'user-uuid', } as Partial as User); diff --git a/services/apps/alcs/src/common/authorization/authorization.service.ts b/services/apps/alcs/src/common/authorization/authorization.service.ts index 9363f06cfd..68ca35024f 100644 --- a/services/apps/alcs/src/common/authorization/authorization.service.ts +++ b/services/apps/alcs/src/common/authorization/authorization.service.ts @@ -196,10 +196,7 @@ export class AuthorizationService { ); if (user.clientRoles.length === 0 && !isPortal) { - await this.userService.sendNewUserRequestEmail( - user.email, - user.bceidGuid ?? user.displayName, - ); + await this.userService.sendNewUserRequestEmail(user.bceidGuid ?? user.displayName, user.email); } } } diff --git a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts index 7563183359..4f23ab0232 100644 --- a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts @@ -55,6 +55,11 @@ import { import { UserDto } from '../../user/user.dto'; import { User } from '../../user/user.entity'; import { Application } from '../../alcs/application/application.entity'; +import { + ApplicationDecisionConditionFinancialInstrument, + InstrumentStatus, +} from '../../alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; +import { ApplicationDecisionConditionFinancialInstrumentDto } from '../../alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto'; @Injectable() export class ApplicationDecisionProfile extends AutomapperProfile { @@ -283,6 +288,19 @@ export class ApplicationDecisionProfile extends AutomapperProfile { : null, ), ), + + forMember( + (dto) => dto.financialInstruments, + mapFrom((entity) => + entity.financialInstruments + ? this.mapper.mapArray( + entity.financialInstruments, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ) + : [], + ), + ), ); createMap( @@ -494,6 +512,8 @@ export class ApplicationDecisionProfile extends AutomapperProfile { createMap(mapper, ApplicationDecision, ApplicationDecisionHomeDto); + createMap(mapper, Application, ApplicationHomeDto); + createMap( mapper, Application, @@ -503,6 +523,42 @@ export class ApplicationDecisionProfile extends AutomapperProfile { mapFrom((ac) => ac.type), ), ); + + createMap( + mapper, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + forMember( + (dto) => dto.issueDate, + mapFrom((entity) => entity.issueDate.getTime()), + ), + forMember( + (dto) => dto.expiryDate, + mapFrom((entity) => (entity.expiryDate ? entity.expiryDate.getTime() : undefined)), + ), + forMember( + (dto) => dto.receivedDate, + mapFrom((entity) => entity.receivedDate.getTime()), + ), + forMember( + (dto) => dto.statusDate, + mapFrom((entity) => + entity.status !== InstrumentStatus.RECEIVED ? entity.statusDate?.getTime() || undefined : undefined, + ), + ), + forMember( + (dto) => dto.explanation, + mapFrom((entity) => entity.explanation || undefined), + ), + forMember( + (dto) => dto.notes, + mapFrom((entity) => entity.notes || undefined), + ), + forMember( + (dto) => dto.instrumentNumber, + mapFrom((entity) => entity.instrumentNumber || undefined), + ), + ); }; } } diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts index 6136f49486..f00a4a6d5b 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts @@ -50,6 +50,11 @@ import { NoticeOfIntentDecisionConditionCardUuidDto, NoticeOfIntentDecisionConditionHomeCardDto, } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.dto'; +import { + InstrumentStatus, + NoticeOfIntentDecisionConditionFinancialInstrument, +} from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentDto } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto'; @Injectable() export class NoticeOfIntentDecisionProfile extends AutomapperProfile { @@ -215,6 +220,19 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile { : null, ), ), + + forMember( + (dto) => dto.financialInstruments, + mapFrom((entity) => + entity.financialInstruments + ? this.mapper.mapArray( + entity.financialInstruments, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ) + : [], + ), + ), ); createMap( @@ -457,6 +475,42 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile { mapFrom((ac) => ac.type), ), ); + + createMap( + mapper, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + forMember( + (dto) => dto.issueDate, + mapFrom((entity) => entity.issueDate.getTime()), + ), + forMember( + (dto) => dto.expiryDate, + mapFrom((entity) => (entity.expiryDate ? entity.expiryDate.getTime() : undefined)), + ), + forMember( + (dto) => dto.receivedDate, + mapFrom((entity) => entity.receivedDate.getTime()), + ), + forMember( + (dto) => dto.statusDate, + mapFrom((entity) => + entity.status !== InstrumentStatus.RECEIVED ? entity.statusDate?.getTime() || undefined : undefined, + ), + ), + forMember( + (dto) => dto.explanation, + mapFrom((entity) => entity.explanation || undefined), + ), + forMember( + (dto) => dto.notes, + mapFrom((entity) => entity.notes || undefined), + ), + forMember( + (dto) => dto.instrumentNumber, + mapFrom((entity) => entity.instrumentNumber || undefined), + ), + ); }; } } diff --git a/services/apps/alcs/src/portal/inbox/inbox.dto.ts b/services/apps/alcs/src/portal/inbox/inbox.dto.ts index e681bb0200..8f66ab0e3e 100644 --- a/services/apps/alcs/src/portal/inbox/inbox.dto.ts +++ b/services/apps/alcs/src/portal/inbox/inbox.dto.ts @@ -92,6 +92,14 @@ export class InboxRequestDto { @IsString() @IsOptional() tagCategoryId?: string; + + @IsString() + @IsOptional() + decisionMaker?: string; + + @IsArray() + @IsOptional() + decisionOutcomes?: string[]; } // typeorm does not transform property names for the status diff --git a/services/apps/alcs/src/portal/public/search/public-search.dto.ts b/services/apps/alcs/src/portal/public/search/public-search.dto.ts index 230f6eb374..d10b857b7a 100644 --- a/services/apps/alcs/src/portal/public/search/public-search.dto.ts +++ b/services/apps/alcs/src/portal/public/search/public-search.dto.ts @@ -112,6 +112,14 @@ export class SearchRequestDto extends PagingRequestDto { @IsArray() @IsOptional() tagCategoryId?: string; + + @IsArray() + @IsOptional() + decisionMaker?: string; + + @IsArray() + @IsOptional() + decisionOutcomes?: string[]; } // typeorm does not transform property names for the status diff --git a/services/apps/alcs/src/providers/email/email.service.ts b/services/apps/alcs/src/providers/email/email.service.ts index 5a0394348d..11074588e6 100644 --- a/services/apps/alcs/src/providers/email/email.service.ts +++ b/services/apps/alcs/src/providers/email/email.service.ts @@ -181,6 +181,14 @@ export class EmailService { let errorMessage = e.message; if (e.response) { errorMessage = e.response.data.detail; + + // Add error details if they exist + if (e.response.data.errors) { + const errorDetails = e.response.data.errors + .map(error => error.message) + .join(', '); + errorMessage = `${errorMessage}: ${errorDetails}`; + } } this.repository.save( diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts b/services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts new file mode 100644 index 0000000000..f972cb2132 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddApplicationConditionFinancialInstrument1739313333099 implements MigrationInterface { + name = 'AddApplicationConditionFinancialInstrument1739313333099'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "alcs"."application_decision_condition_financial_instrument_type_enum" AS ENUM('Bank Draft', 'Certified Cheque', 'EFT', 'Irrevocable Letter of Credit', 'Other', 'Safekeeping Agreement')`, + ); + await queryRunner.query( + `CREATE TYPE "alcs"."application_decision_condition_financial_instrument_held_by_enum" AS ENUM('ALC', 'Ministry')`, + ); + await queryRunner.query( + `CREATE TYPE "alcs"."application_decision_condition_financial_instrument_status_enum" AS ENUM('Received', 'Released', 'Cashed', 'Replaced')`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."application_decision_condition_financial_instrument" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "security_holder_payee" character varying NOT NULL, "type" "alcs"."application_decision_condition_financial_instrument_type_enum" NOT NULL, "issue_date" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry_date" TIMESTAMP WITH TIME ZONE, "amount" numeric(12,2) NOT NULL, "bank" character varying NOT NULL, "instrument_number" character varying, "held_by" "alcs"."application_decision_condition_financial_instrument_held_by_enum" NOT NULL, "received_date" TIMESTAMP WITH TIME ZONE NOT NULL, "notes" text, "status" "alcs"."application_decision_condition_financial_instrument_status_enum" NOT NULL DEFAULT 'Received', "status_date" TIMESTAMP WITH TIME ZONE, "explanation" text, "condition_uuid" uuid, CONSTRAINT "PK_4474d61a96a50f86d8f1b6cce28" PRIMARY KEY ("uuid"))`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."application_decision_condition_financial_instrument" IS 'Instrument for Financial Security Conditions'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_financial_instrument" ADD CONSTRAINT "FK_7ee6009fecca8304ed53608bdc5" FOREIGN KEY ("condition_uuid") REFERENCES "alcs"."application_decision_condition"("uuid") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_financial_instrument" DROP CONSTRAINT "FK_7ee6009fecca8304ed53608bdc5"`, + ); + await queryRunner.query(`COMMENT ON TABLE "alcs"."application_decision_condition_financial_instrument" IS NULL`); + await queryRunner.query(`DROP TABLE "alcs"."application_decision_condition_financial_instrument"`); + await queryRunner.query(`DROP TYPE "alcs"."application_decision_condition_financial_instrument_status_enum"`); + await queryRunner.query(`DROP TYPE "alcs"."application_decision_condition_financial_instrument_held_by_enum"`); + await queryRunner.query(`DROP TYPE "alcs"."application_decision_condition_financial_instrument_type_enum"`); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1740423770890-add_order_properties.ts b/services/apps/alcs/src/providers/typeorm/migrations/1740423770890-add_order_properties.ts new file mode 100644 index 0000000000..32fa73a9c6 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1740423770890-add_order_properties.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddOrderProperties1740423770890 implements MigrationInterface { + name = 'AddOrderProperties1740423770890' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "alcs"."application_decision_condition" ADD "order" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision_condition" ADD "order" integer NOT NULL DEFAULT '0'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision_condition" DROP COLUMN "order"`); + await queryRunner.query(`ALTER TABLE "alcs"."application_decision_condition" DROP COLUMN "order"`); + } + +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1740529203706-add_notice_of_intent_condition_financial_instrument.ts b/services/apps/alcs/src/providers/typeorm/migrations/1740529203706-add_notice_of_intent_condition_financial_instrument.ts new file mode 100644 index 0000000000..1b660552f0 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1740529203706-add_notice_of_intent_condition_financial_instrument.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddNoticeOfIntentConditionFinancialInstrument1740529203706 implements MigrationInterface { + name = 'AddNoticeOfIntentConditionFinancialInstrument1740529203706'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_type_enum" AS ENUM('Bank Draft', 'Certified Cheque', 'EFT', 'Irrevocable Letter of Credit', 'Other', 'Safekeeping Agreement')`, + ); + await queryRunner.query( + `CREATE TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_held_by_enum" AS ENUM('ALC', 'Ministry')`, + ); + await queryRunner.query( + `CREATE TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_status_enum" AS ENUM('Received', 'Released', 'Cashed', 'Replaced')`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "security_holder_payee" character varying NOT NULL, "type" "alcs"."notice_of_intent_decision_condition_financial_instrument_type_enum" NOT NULL, "issue_date" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry_date" TIMESTAMP WITH TIME ZONE, "amount" numeric(12,2) NOT NULL, "bank" character varying NOT NULL, "instrument_number" character varying, "held_by" "alcs"."notice_of_intent_decision_condition_financial_instrument_held_by_enum" NOT NULL, "received_date" TIMESTAMP WITH TIME ZONE NOT NULL, "notes" text, "status" "alcs"."notice_of_intent_decision_condition_financial_instrument_status_enum" NOT NULL DEFAULT 'Received', "status_date" TIMESTAMP WITH TIME ZONE, "explanation" text, "condition_uuid" uuid, CONSTRAINT "PK_cd31b04c238e6ccf3e6ac3e37f0" PRIMARY KEY ("uuid"))`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument" IS 'Instrument for Financial Security Conditions'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument" ADD CONSTRAINT "FK_6dfce6b06252ca93fb88a21c471" FOREIGN KEY ("condition_uuid") REFERENCES "alcs"."notice_of_intent_decision_condition"("uuid") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument" DROP CONSTRAINT "FK_6dfce6b06252ca93fb88a21c471"`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument" IS NULL`, + ); + await queryRunner.query(`DROP TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument"`); + await queryRunner.query(`DROP TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_status_enum"`); + await queryRunner.query(`DROP TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_held_by_enum"`); + await queryRunner.query(`DROP TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_type_enum"`); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1741219585820-make_user_email_nullable.ts b/services/apps/alcs/src/providers/typeorm/migrations/1741219585820-make_user_email_nullable.ts new file mode 100644 index 0000000000..8a4c95863b --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1741219585820-make_user_email_nullable.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MakeUserEmailNullable1741219585820 implements MigrationInterface { + name = 'MakeUserEmailNullable1741219585820' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "alcs"."user" ALTER COLUMN "email" DROP NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "alcs"."user" ALTER COLUMN "email" SET NOT NULL`); + } + +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1741287102799-rename_financial_instrument_enums.ts b/services/apps/alcs/src/providers/typeorm/migrations/1741287102799-rename_financial_instrument_enums.ts new file mode 100644 index 0000000000..c32b8495ed --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1741287102799-rename_financial_instrument_enums.ts @@ -0,0 +1,53 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameFinancialInstrumentEnums1741287102799 implements MigrationInterface { + name = 'RenameFinancialInstrumentEnums1741287102799'; + + public async up(queryRunner: QueryRunner): Promise { + // Application instrument types - direct rename + await queryRunner.query( + `ALTER TYPE "alcs"."application_decision_condition_financial_instrument_type_enum" RENAME TO "application_instrument_type"`, + ); + await queryRunner.query( + `ALTER TYPE "alcs"."application_decision_condition_financial_instrument_held_by_enu" RENAME TO "application_instrument_held_by"`, + ); + await queryRunner.query( + `ALTER TYPE "alcs"."application_decision_condition_financial_instrument_status_enum" RENAME TO "application_instrument_status"`, + ); + + // Notice of intent instrument types - direct rename + await queryRunner.query( + `ALTER TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_type_e" RENAME TO "noi_instrument_type"`, + ); + await queryRunner.query( + `ALTER TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_held_b" RENAME TO "noi_instrument_held_by"`, + ); + await queryRunner.query( + `ALTER TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_status" RENAME TO "noi_instrument_status"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Revert notice of intent instrument types - direct rename + await queryRunner.query( + `ALTER TYPE "alcs"."noi_instrument_status" RENAME TO "notice_of_intent_decision_condition_financial_instrument_status"`, + ); + await queryRunner.query( + `ALTER TYPE "alcs"."noi_instrument_held_by" RENAME TO "notice_of_intent_decision_condition_financial_instrument_held_b"`, + ); + await queryRunner.query( + `ALTER TYPE "alcs"."noi_instrument_type" RENAME TO "notice_of_intent_decision_condition_financial_instrument_type_e"`, + ); + + // Revert application instrument types - direct rename + await queryRunner.query( + `ALTER TYPE "alcs"."application_instrument_status" RENAME TO "application_decision_condition_financial_instrument_status_enum"`, + ); + await queryRunner.query( + `ALTER TYPE "alcs"."application_instrument_held_by" RENAME TO "application_decision_condition_financial_instrument_held_by_enu"`, + ); + await queryRunner.query( + `ALTER TYPE "alcs"."application_instrument_type" RENAME TO "application_decision_condition_financial_instrument_type_enum"`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1741289414578-add_default_to_app_instrument_status.ts b/services/apps/alcs/src/providers/typeorm/migrations/1741289414578-add_default_to_app_instrument_status.ts new file mode 100644 index 0000000000..e5a723ef4e --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1741289414578-add_default_to_app_instrument_status.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddDefaultToAppInstrumentStatus1741289414578 implements MigrationInterface { + name = 'AddDefaultToAppInstrumentStatus1741289414578' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "alcs"."application_decision_condition_financial_instrument" ALTER COLUMN "status" SET DEFAULT 'Received'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "alcs"."application_decision_condition_financial_instrument" ALTER COLUMN "status" DROP DEFAULT`); + } + +} diff --git a/services/apps/alcs/src/user/user.dto.ts b/services/apps/alcs/src/user/user.dto.ts index f856ba31cb..c281457e5e 100644 --- a/services/apps/alcs/src/user/user.dto.ts +++ b/services/apps/alcs/src/user/user.dto.ts @@ -49,7 +49,7 @@ export class UserDto extends UpdateUserDto { } export class CreateUserDto { - email: string; + email?: string; name?: string; displayName: string; givenName?: string; @@ -79,7 +79,7 @@ export class AssigneeDto { mentionLabel: string; @AutoMap() - email: string; + email?: string; @AutoMap() clientRoles: string[]; diff --git a/services/apps/alcs/src/user/user.entity.ts b/services/apps/alcs/src/user/user.entity.ts index 65ccaf1d56..2c3d515e41 100644 --- a/services/apps/alcs/src/user/user.entity.ts +++ b/services/apps/alcs/src/user/user.entity.ts @@ -21,8 +21,8 @@ export class User extends Base { } @AutoMap() - @Column() - email: string; + @Column({ nullable: true }) + email?: string; @AutoMap() @Column() diff --git a/services/apps/alcs/src/user/user.service.spec.ts b/services/apps/alcs/src/user/user.service.spec.ts index 9277972bca..12c509fe2b 100644 --- a/services/apps/alcs/src/user/user.service.spec.ts +++ b/services/apps/alcs/src/user/user.service.spec.ts @@ -151,7 +151,7 @@ describe('UserService', () => { const body = `A new user ${email}: ${userIdentifier} has requested access to ALCS.
CSS`; - await service.sendNewUserRequestEmail(email, userIdentifier); + await service.sendNewUserRequestEmail(userIdentifier, email); expect(emailServiceMock.sendEmail).toBeCalledWith({ to: config.get('EMAIL.DEFAULT_ADMINS'), diff --git a/services/apps/alcs/src/user/user.service.ts b/services/apps/alcs/src/user/user.service.ts index cbabe36aa0..7d0fd79038 100644 --- a/services/apps/alcs/src/user/user.service.ts +++ b/services/apps/alcs/src/user/user.service.ts @@ -124,11 +124,12 @@ export class UserService { return null; } - async sendNewUserRequestEmail(email: string, userIdentifier: string) { + async sendNewUserRequestEmail(userIdentifier: string, email?: string) { + const userText = email ? `${email}: ${userIdentifier}` : `${userIdentifier}`; const env = this.config.get('ENV'); const prefix = env === 'production' ? '' : `[${env}]`; const subject = `${prefix} Access Requested to ALCS`; - const body = `A new user ${email}: ${userIdentifier} has requested access to ALCS.
+ const body = `A new user ${userText} has requested access to ALCS.
CSS`; await this.emailService.sendEmail({ diff --git a/services/apps/alcs/src/utils/search/application-search-filters.ts b/services/apps/alcs/src/utils/search/application-search-filters.ts index 2cdc167f28..66e60a7902 100644 --- a/services/apps/alcs/src/utils/search/application-search-filters.ts +++ b/services/apps/alcs/src/utils/search/application-search-filters.ts @@ -66,6 +66,34 @@ export const APP_SEARCH_FILTERS = { }) .getMany(); }, + addDecisionMakerResults: (searchDto: SearchRequestDto | InboxRequestDto, appRepository: Repository) => { + return appRepository + .createQueryBuilder('app') + .select('app.fileNumber') + .leftJoin('application_decision', 'application_decision', 'application_decision.application_uuid = app.uuid') + .leftJoin( + 'application_decision_maker_code', + 'application_decision_maker_code', + 'application_decision_maker_code.code = application_decision.decision_maker_code', + ) + .where('application_decision_maker_code.code IN (:code)', { + code: searchDto.decisionMaker, + }) + .getMany(); + }, + addDecisionOutcomeResults: ( + searchDto: SearchRequestDto | InboxRequestDto, + appRepository: Repository, + ) => { + return appRepository + .createQueryBuilder('app') + .select('app.fileNumber') + .leftJoin('application_decision', 'application_decision', 'application_decision.application_uuid = app.uuid') + .where('application_decision.outcome_code IN (:...outcomeCodes)', { + outcomeCodes: searchDto.decisionOutcomes, + }) + .getMany(); + }, addNameResults: ( searchDto: SearchRequestDto | InboxRequestDto, applicationSubmissionRepository: Repository, diff --git a/services/apps/alcs/src/utils/search/notice-of-intent-search-filters.ts b/services/apps/alcs/src/utils/search/notice-of-intent-search-filters.ts index e951c81c56..ebaa0e7bd6 100644 --- a/services/apps/alcs/src/utils/search/notice-of-intent-search-filters.ts +++ b/services/apps/alcs/src/utils/search/notice-of-intent-search-filters.ts @@ -60,6 +60,23 @@ export const NOI_SEARCH_FILTERS = { }) .getMany(); }, + addDecisionOutcomeResults: ( + searchDto: SearchRequestDto | InboxRequestDto, + appRepository: Repository, + ) => { + return appRepository + .createQueryBuilder('noi') + .select('noi.fileNumber') + .leftJoin( + 'notice_of_intent_decision', + 'notice_of_intent_decision', + 'notice_of_intent_decision.notice_of_intent_uuid = noi.uuid', + ) + .where('notice_of_intent_decision.outcome_code IN (:...outcomeCodes)', { + outcomeCodes: searchDto.decisionOutcomes, + }) + .getMany(); + }, addNameResults: ( searchDto: SearchRequestDto | InboxRequestDto, noiSubmissionRepository: Repository, diff --git a/services/apps/alcs/test/mocks/mockEntities.ts b/services/apps/alcs/test/mocks/mockEntities.ts index 026063495f..01992a9238 100644 --- a/services/apps/alcs/test/mocks/mockEntities.ts +++ b/services/apps/alcs/test/mocks/mockEntities.ts @@ -27,6 +27,16 @@ import { Tag } from '../../src/alcs/tag/tag.entity'; import { NoticeOfIntent } from '../../src/alcs/notice-of-intent/notice-of-intent.entity'; import { NoticeOfIntentType } from '../../src/alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity'; import { ApplicationDecisionConditionCard } from '../../src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.entity'; +import { ApplicationDecisionConditionType } from '../../src/alcs/application-decision/application-decision-condition/application-decision-condition-code.entity'; +import { ApplicationDecisionCondition } from '../../src/alcs/application-decision/application-decision-condition/application-decision-condition.entity'; +import { + ApplicationDecisionConditionFinancialInstrument, + HeldBy, + InstrumentStatus, + InstrumentType, +} from '../../src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; +import { NoticeOfIntentDecisionConditionFinancialInstrument } from '../../src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; +import { NoticeOfIntentDecisionConditionType } from '../../src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity'; const initCardStatusMockEntity = (): CardStatus => { const cardStatus = new CardStatus(); @@ -411,6 +421,98 @@ const initMockApplicationDecisionConditionCard = ( return conditionCard; }; +const initApplicationDecisionConditionTypeMockEntity = (code?: string): ApplicationDecisionConditionType => { + const conditionType = new ApplicationDecisionConditionType(); + conditionType.code = code ? code : 'type_1'; + conditionType.description = 'condition desc 1'; + conditionType.label = 'condition_label'; + conditionType.isActive = true; + conditionType.isComponentToConditionChecked = true; + conditionType.isDescriptionChecked = true; + conditionType.isAdministrativeFeeAmountChecked = false; + conditionType.isAdministrativeFeeAmountRequired = null; + conditionType.administrativeFeeAmount = null; + conditionType.isDateChecked = false; + conditionType.isDateRequired = null; + conditionType.dateType = null; + conditionType.singleDateLabel = null; + conditionType.isSecurityAmountChecked = false; + conditionType.isSecurityAmountRequired = null; + conditionType.auditCreatedAt = new Date(1, 1, 1, 1, 1, 1, 1); + conditionType.auditUpdatedAt = new Date(1, 1, 1, 1, 1, 1, 1); + + return conditionType; +}; + +const initApplicationDecisionConditionFinancialInstrumentMockEntity = ( + payee?: string, + bank?: string, + instrumentNumber?: string, + condition?: ApplicationDecisionCondition, +): ApplicationDecisionConditionFinancialInstrument => { + const instrument = new ApplicationDecisionConditionFinancialInstrument(); + instrument.securityHolderPayee = 'fake-payee'; + instrument.type = InstrumentType.BANK_DRAFT; + instrument.issueDate = new Date(2022, 1, 1); + instrument.expiryDate = new Date(2023, 1, 1); + instrument.amount = 1000.0; + instrument.bank = 'fake-bank'; + instrument.instrumentNumber = '123456'; + instrument.heldBy = HeldBy.ALC; + instrument.receivedDate = new Date(2022, 1, 1); + instrument.notes = 'fake-notes'; + instrument.status = InstrumentStatus.RECEIVED; + instrument.statusDate = new Date(2022, 1, 1); + instrument.explanation = 'fake-explanation'; + instrument.condition = condition ?? new ApplicationDecisionCondition(); + return instrument; +}; + +const initNoticeOfIntentDecisionConditionTypeMockEntity = (code?: string): NoticeOfIntentDecisionConditionType => { + const conditionType = new NoticeOfIntentDecisionConditionType(); + conditionType.code = code ? code : 'type_1'; + conditionType.description = 'condition desc 1'; + conditionType.label = 'condition_label'; + conditionType.isActive = true; + conditionType.isComponentToConditionChecked = true; + conditionType.isDescriptionChecked = true; + conditionType.isAdministrativeFeeAmountChecked = false; + conditionType.isAdministrativeFeeAmountRequired = null; + conditionType.administrativeFeeAmount = null; + conditionType.isDateChecked = false; + conditionType.isDateRequired = null; + conditionType.dateType = null; + conditionType.singleDateLabel = null; + conditionType.isSecurityAmountChecked = false; + conditionType.isSecurityAmountRequired = null; + conditionType.auditCreatedAt = new Date(1, 1, 1, 1, 1, 1, 1); + conditionType.auditUpdatedAt = new Date(1, 1, 1, 1, 1, 1, 1); + + return conditionType; +}; + +const initNoticeOfIntentDecisionConditionFinancialInstrumentMockEntity = ( + payee?: string, + bank?: string, + instrumentNumber?: string, +): NoticeOfIntentDecisionConditionFinancialInstrument => { + const instrument = new NoticeOfIntentDecisionConditionFinancialInstrument(); + instrument.securityHolderPayee = 'fake-payee'; + instrument.type = InstrumentType.BANK_DRAFT; + instrument.issueDate = new Date(2022, 1, 1); + instrument.expiryDate = new Date(2023, 1, 1); + instrument.amount = 1000.0; + instrument.bank = 'fake-bank'; + instrument.instrumentNumber = '123456'; + instrument.heldBy = HeldBy.ALC; + instrument.receivedDate = new Date(2022, 1, 1); + instrument.notes = 'fake-notes'; + instrument.status = InstrumentStatus.RECEIVED; + instrument.statusDate = new Date(2022, 1, 1); + instrument.explanation = 'fake-explanation'; + return instrument; +}; + export { initCardStatusMockEntity, initApplicationMockEntity, @@ -436,4 +538,8 @@ export { initNoticeOfIntentMockEntity, initNoticeOfIntentWithTagsMockEntity, initMockApplicationDecisionConditionCard, + initApplicationDecisionConditionTypeMockEntity, + initApplicationDecisionConditionFinancialInstrumentMockEntity, + initNoticeOfIntentDecisionConditionTypeMockEntity, + initNoticeOfIntentDecisionConditionFinancialInstrumentMockEntity, };