From 911bfa56669e7505f23c0537fe93e663e654ad13 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 10 Aug 2023 15:35:57 -0700 Subject: [PATCH 1/5] Add NOI Stepper and Step 1 * Add Parcels and Owners * Change ApplicationOwnerType to OwnerType and re-use across both Apps and NOIs * Add NOI Document controller and service for portal --- .../application-details.component.html | 10 +- .../application-details.component.ts | 17 +- .../excl-details/excl-details.component.ts | 3 +- .../incl-details/incl-details.component.ts | 3 +- .../naru-details/naru-details.component.ts | 3 +- .../pfrs-details/pfrs-details.component.ts | 3 +- .../pofo-details/pofo-details.component.ts | 3 +- .../roso-details/roso-details.component.ts | 3 +- .../subd-details/subd-details.component.ts | 3 +- .../tur-details/tur-details.component.ts | 3 +- .../edit-submission/files-step.partial.ts | 4 +- .../other-attachments.component.ts | 10 +- .../other-parcels/other-parcels.component.ts | 7 +- ...pplication-crown-owner-dialog.component.ts | 6 +- .../application-owner-dialog.component.ts | 20 +- .../application-owners-dialog.component.ts | 5 +- .../parcel-details.component.ts | 9 +- .../parcel-entry/parcel-entry.component.ts | 13 +- .../parcel-owners/parcel-owners.component.ts | 5 +- .../primary-contact.component.ts | 25 +- .../excl-proposal/excl-proposal.component.ts | 6 +- .../incl-proposal/incl-proposal.component.ts | 6 +- .../naru-proposal/naru-proposal.component.ts | 6 +- .../pfrs-proposal/pfrs-proposal.component.ts | 6 +- .../pofo-proposal/pofo-proposal.component.ts | 6 +- .../roso-proposal/roso-proposal.component.ts | 6 +- .../subd-proposal/subd-proposal.component.ts | 6 +- .../tur-proposal/tur-proposal.component.ts | 6 +- .../review-attachments.component.ts | 7 +- .../review-submit-fng.component.ts | 3 +- .../review-submit/review-submit.component.ts | 3 +- .../lfng-review/lfng-review.component.ts | 7 +- .../edit-submission-base.module.ts | 48 ++ .../edit-submission.component.html | 71 ++- .../edit-submission.component.scss | 378 +++++++++++++ .../edit-submission.component.spec.ts | 27 +- .../edit-submission.component.ts | 150 +++++- .../edit-submission/edit-submission.module.ts | 12 +- .../delete-parcel-dialog.component.html | 36 ++ .../delete-parcel-dialog.component.scss | 24 + .../delete-parcel-dialog.component.spec.ts | 48 ++ .../delete-parcel-dialog.component.ts | 52 ++ .../parcels/parcel-details.component.html | 68 +++ .../parcels/parcel-details.component.scss | 0 .../parcels/parcel-details.component.spec.ts | 70 +++ .../parcels/parcel-details.component.ts | 173 ++++++ ...l-entry-confirmation-dialog.component.html | 35 ++ ...l-entry-confirmation-dialog.component.scss | 24 + ...ntry-confirmation-dialog.component.spec.ts | 35 ++ ...cel-entry-confirmation-dialog.component.ts | 33 ++ .../parcel-entry/parcel-entry.component.html | 372 +++++++++++++ .../parcel-entry/parcel-entry.component.scss | 96 ++++ .../parcel-entry.component.spec.ts | 81 +++ .../parcel-entry/parcel-entry.component.ts | 501 ++++++++++++++++++ .../parcel-owners.component.html | 73 +++ .../parcel-owners.component.scss | 14 + .../parcel-owners.component.spec.ts | 41 ++ .../parcel-owners/parcel-owners.component.ts | 91 ++++ .../edit-submission/step.partial.ts | 34 ++ .../application-document.dto.ts | 44 +- .../application-document.service.spec.ts | 2 +- .../application-document.service.ts | 8 +- .../application-owner.dto.ts | 17 +- .../application-owner.service.ts | 2 +- .../application-parcel.service.ts | 12 +- .../src/app/services/code/code.service.ts | 4 +- .../app/services/document/document.service.ts | 2 +- .../notice-of-intent-document.dto.ts | 18 + .../notice-of-intent-document.service.spec.ts | 91 ++++ .../notice-of-intent-document.service.ts | 91 ++++ .../notice-of-intent-owner.dto.ts | 57 ++ .../notice-of-intent-owner.service.spec.ts | 189 +++++++ .../notice-of-intent-owner.service.ts | 144 +++++ .../notice-of-intent-parcel.dto.ts | 29 + .../notice-of-intent-parcel.service.spec.ts | 102 ++++ .../notice-of-intent-parcel.service.ts | 107 ++++ .../notice-of-intent-submission.dto.ts | 2 + .../src/app/shared/dto/document.dto.ts | 41 ++ .../src/app/shared/dto/owner.dto.ts | 13 + .../application-document.service.ts | 4 +- ...tice-of-intent-document.controller.spec.ts | 2 - .../notice-of-intent-document.service.ts | 68 +-- .../notice-of-intent.service.ts | 4 +- .../application-owner.automapper.profile.ts | 5 +- ...tice-of-intent-owner.automapper.profile.ts | 82 +++ ...ice-of-intent-parcel.automapper.profile.ts | 85 +++ .../common/owner-type/owner-type.entity.ts | 23 + .../application-submission-draft.module.ts | 4 +- ...ation-submission-review.controller.spec.ts | 4 +- ...pplication-submission-review.controller.ts | 10 +- .../application-owner-type.entity.ts | 12 - .../application-owner.controller.spec.ts | 14 +- .../application-owner.controller.ts | 15 +- .../application-owner.dto.ts | 18 +- .../application-owner.entity.ts | 6 +- .../application-owner.service.spec.ts | 8 +- .../application-owner.service.ts | 14 +- ...ation-submission-validator.service.spec.ts | 34 +- ...pplication-submission-validator.service.ts | 16 +- .../application-submission.module.ts | 4 +- .../alcs/src/portal/code/code.controller.ts | 7 +- ...tice-of-intent-document.controller.spec.ts | 179 +++++++ .../notice-of-intent-document.controller.ts | 162 ++++++ .../notice-of-intent-document.dto.ts | 30 ++ .../notice-of-intent-document.module.ts | 17 + .../notice-of-intent-owner.controller.spec.ts | 345 ++++++++++++ .../notice-of-intent-owner.controller.ts | 259 +++++++++ .../notice-of-intent-owner.dto.ts | 138 +++++ .../notice-of-intent-owner.entity.ts | 77 +++ .../notice-of-intent-owner.service.spec.ts | 341 ++++++++++++ .../notice-of-intent-owner.service.ts | 294 ++++++++++ ...-of-intent-parcel-ownership-type.entity.ts | 5 + ...notice-of-intent-parcel.controller.spec.ts | 165 ++++++ .../notice-of-intent-parcel.controller.ts | 158 ++++++ .../notice-of-intent-parcel.dto.ts | 147 +++++ .../notice-of-intent-parcel.entity.ts | 147 +++++ .../notice-of-intent-parcel.service.spec.ts | 248 +++++++++ .../notice-of-intent-parcel.service.ts | 140 +++++ .../notice-of-intent-submission.entity.ts | 7 + .../notice-of-intent-submission.module.ts | 34 +- .../notice-of-intent-submission.service.ts | 4 + .../generate-submission-document.service.ts | 4 +- .../apps/alcs/src/portal/portal.module.ts | 3 + ...5-add_noi_submission_parcels_and_owners.ts | 111 ++++ ...691705084117-seed_noi_parcel_owner_type.ts | 14 + 125 files changed, 6910 insertions(+), 343 deletions(-) create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/step.partial.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.dto.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts create mode 100644 portal-frontend/src/app/shared/dto/document.dto.ts create mode 100644 portal-frontend/src/app/shared/dto/owner.dto.ts create mode 100644 services/apps/alcs/src/common/automapper/notice-of-intent-owner.automapper.profile.ts create mode 100644 services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts create mode 100644 services/apps/alcs/src/common/owner-type/owner-type.entity.ts delete mode 100644 services/apps/alcs/src/portal/application-submission/application-owner/application-owner-type/application-owner-type.entity.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.dto.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.module.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691692339215-add_noi_submission_parcels_and_owners.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691705084117-seed_noi_parcel_owner_type.ts diff --git a/portal-frontend/src/app/features/applications/application-details/application-details.component.html b/portal-frontend/src/app/features/applications/application-details/application-details.component.html index 8c1e4e2548..de1ad9841b 100644 --- a/portal-frontend/src/app/features/applications/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/applications/application-details/application-details.component.html @@ -42,18 +42,18 @@

3. Primary Contact

Organization (optional) - Ministry/Department Responsible - Department + Ministry/Department Responsible + Department
{{ primaryContact?.organizationName }}
diff --git a/portal-frontend/src/app/features/applications/application-details/application-details.component.ts b/portal-frontend/src/app/features/applications/application-details/application-details.component.ts index c8ac8740c3..da184ade67 100644 --- a/portal-frontend/src/app/features/applications/application-details/application-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/application-details.component.ts @@ -1,17 +1,15 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { APPLICATION_OWNER, ApplicationOwnerDetailedDto } from '../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDetailedDto } from '../../../services/application-owner/application-owner.dto'; import { PARCEL_TYPE } from '../../../services/application-parcel/application-parcel.dto'; import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; import { LocalGovernmentDto } from '../../../services/code/code.dto'; import { CodeService } from '../../../services/code/code.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../shared/dto/owner.dto'; @Component({ selector: 'app-application-details', @@ -37,7 +35,7 @@ export class ApplicationDetailsComponent implements OnInit, OnDestroy { otherFiles: ApplicationDocumentDto[] = []; needsAuthorizationLetter = true; appDocuments: ApplicationDocumentDto[] = []; - APPLICATION_OWNER = APPLICATION_OWNER; + OWNER_TYPE = OWNER_TYPE; private localGovernments: LocalGovernmentDto[] = []; private otherFileTypes = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; @@ -57,11 +55,10 @@ export class ApplicationDetailsComponent implements OnInit, OnDestroy { this.populateLocalGovernment(app.localGovernmentUuid); this.needsAuthorizationLetter = - !(this.primaryContact?.type.code === APPLICATION_OWNER.GOVERNMENT) && + !(this.primaryContact?.type.code === OWNER_TYPE.GOVERNMENT) && !( app.owners.length === 1 && - (app.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || - app.owners[0].type.code === APPLICATION_OWNER.GOVERNMENT) + (app.owners[0].type.code === OWNER_TYPE.INDIVIDUAL || app.owners[0].type.code === OWNER_TYPE.GOVERNMENT) ); } }); diff --git a/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts b/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts index 0d9ce89d03..cb7cd34589 100644 --- a/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts @@ -2,7 +2,8 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-excl-details', diff --git a/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts b/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts index b81fd9271c..758c5d32fd 100644 --- a/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts @@ -3,8 +3,9 @@ import { Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { AuthenticationService } from '../../../../services/authentication/authentication.service'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-incl-details', diff --git a/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts index bcbb2644a9..50b6c278e4 100644 --- a/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-naru-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts index d719c83504..934048d407 100644 --- a/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-pfrs-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts index c412d258e5..f2f1576b33 100644 --- a/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-pofo-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts index 7b4d0d5c5e..1d7f7db422 100644 --- a/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-roso-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts index 61ee6278fe..295e9ab999 100644 --- a/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts @@ -1,10 +1,11 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { PARCEL_TYPE } from '../../../../services/application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-subd-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts b/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts index dc196ca276..6444c3b03d 100644 --- a/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-tur-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts b/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts index 2840e92774..c583e6b74d 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; import { FileHandle } from '../../../shared/file-drag-drop/drag-drop.directive'; import { RemoveFileConfirmationDialogComponent } from '../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; import { StepComponent } from './step.partial'; @@ -18,6 +19,7 @@ export abstract class FilesStepComponent extends StepComponent { DOCUMENT_TYPE = DOCUMENT_TYPE; protected fileId = ''; + protected abstract save(): Promise; protected constructor(protected applicationDocumentService: ApplicationDocumentService, protected dialog: MatDialog) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts index 2984985c68..8e26372636 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts @@ -5,14 +5,12 @@ import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, - ApplicationDocumentTypeDto, ApplicationDocumentUpdateDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { CodeService } from '../../../../services/code/code.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../shared/dto/document.dto'; import { EditApplicationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; @@ -27,13 +25,13 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI currentStep = EditApplicationSteps.Attachments; displayedColumns = ['type', 'description', 'fileName', 'actions']; - selectableTypes: ApplicationDocumentTypeDto[] = []; + selectableTypes: DocumentTypeDto[] = []; otherFiles: ApplicationDocumentDto[] = []; private isDirty = false; form = new FormGroup({} as any); - private documentCodes: ApplicationDocumentTypeDto[] = []; + private documentCodes: DocumentTypeDto[] = []; constructor( private router: Router, @@ -117,7 +115,7 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI private async loadDocumentCodes() { const codes = await this.codeService.loadCodes(); - this.documentCodes = codes.applicationDocumentTypes; + this.documentCodes = codes.documentTypes; this.selectableTypes = this.documentCodes.filter((code) => USER_CONTROLLED_TYPES.includes(code.code)); } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts index d15c92e1f3..e9476cdbc2 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts @@ -1,11 +1,9 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MatDialog } from '@angular/material/dialog'; -import { Router } from '@angular/router'; import { BehaviorSubject, takeUntil } from 'rxjs'; import { - APPLICATION_OWNER, ApplicationOwnerDetailedDto, ApplicationOwnerDto, } from '../../../../services/application-owner/application-owner.dto'; @@ -18,6 +16,7 @@ import { ApplicationParcelService } from '../../../../services/application-parce import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { ToastService } from '../../../../services/toast/toast.service'; +import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; import { getLetterCombinations } from '../../../../shared/utils/number-to-letter-helper'; import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; @@ -71,7 +70,7 @@ export class OtherParcelsComponent extends StepComponent implements OnInit, OnDe this.fileId = application.fileNumber; this.submissionUuid = application.uuid; const nonAgentOwners = application.owners.filter( - (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) ); this.owners = nonAgentOwners.map((o) => ({ ...o, diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts index fc2981a6fe..8aba0af222 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts @@ -2,12 +2,12 @@ import { Component, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { - APPLICATION_OWNER, ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, } from '../../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; @Component({ selector: 'app-application-crown-owner-dialog', @@ -61,7 +61,7 @@ export class ApplicationCrownOwnerDialogComponent { lastName: this.lastName.getRawValue() || undefined, email: this.email.getRawValue()!, phoneNumber: this.phoneNumber.getRawValue()!, - typeCode: APPLICATION_OWNER.CROWN, + typeCode: OWNER_TYPE.CROWN, applicationSubmissionUuid: this.data.submissionUuid, }; @@ -80,7 +80,7 @@ export class ApplicationCrownOwnerDialogComponent { lastName: this.lastName.getRawValue(), email: this.email.getRawValue()!, phoneNumber: this.phoneNumber.getRawValue()!, - typeCode: APPLICATION_OWNER.CROWN, + typeCode: OWNER_TYPE.CROWN, }; if (this.existingUuid) { const res = await this.appOwnerService.update(this.existingUuid, updateDto); diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts index 37fe30f179..0826020249 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts @@ -2,21 +2,17 @@ import { Component, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { - ApplicationDocumentDto, - ApplicationDocumentTypeDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { - APPLICATION_OWNER, ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, } from '../../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; import { CodeService } from '../../../../../services/code/code.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; @@ -26,8 +22,8 @@ import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submis styleUrls: ['./application-owner-dialog.component.scss'], }) export class ApplicationOwnerDialogComponent { - OWNER_TYPE = APPLICATION_OWNER; - type = new FormControl(APPLICATION_OWNER.INDIVIDUAL); + OWNER_TYPE = OWNER_TYPE; + type = new FormControl(OWNER_TYPE.INDIVIDUAL); firstName = new FormControl('', [Validators.required]); lastName = new FormControl('', [Validators.required]); organizationName = new FormControl(''); @@ -49,7 +45,7 @@ export class ApplicationOwnerDialogComponent { corporateSummary: this.corporateSummary, }); private pendingFile: File | undefined; - private documentCodes: ApplicationDocumentTypeDto[] = []; + private documentCodes: DocumentTypeDto[] = []; constructor( private dialogRef: MatDialogRef, @@ -87,7 +83,7 @@ export class ApplicationOwnerDialogComponent { } onChangeType($event: MatButtonToggleChange) { - if ($event.value === APPLICATION_OWNER.ORGANIZATION) { + if ($event.value === OWNER_TYPE.ORGANIZATION) { this.organizationName.setValidators([Validators.required]); this.corporateSummary.setValidators([Validators.required]); } else { @@ -210,6 +206,6 @@ export class ApplicationOwnerDialogComponent { private async loadDocumentCodes() { const codes = await this.codeService.loadCodes(); - this.documentCodes = codes.applicationDocumentTypes; + this.documentCodes = codes.documentTypes; } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts index 83c7335b90..8a884734aa 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts @@ -1,7 +1,8 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; @Component({ selector: 'app-application-owner-dialog', @@ -32,7 +33,7 @@ export class ApplicationOwnersDialogComponent { const updatedOwners = await this.applicationOwnerService.fetchBySubmissionId(this.submissionUuid); if (updatedOwners) { this.owners = updatedOwners.filter( - (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) ); this.isDirty = true; } diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts index 6c2f6e4cee..3cc45bef0e 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts @@ -1,8 +1,8 @@ -import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { BehaviorSubject, takeUntil } from 'rxjs'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; import { ApplicationParcelDto, @@ -11,6 +11,7 @@ import { } from '../../../../services/application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; import { ToastService } from '../../../../services/toast/toast.service'; +import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../edit-submission.component'; import { StepComponent } from '../step.partial'; @@ -50,7 +51,7 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft this.submissionUuid = applicationSubmission.uuid; this.loadParcels(); const parcelOwners = applicationSubmission.owners.filter( - (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) ); this.$owners.next(parcelOwners); } @@ -164,7 +165,7 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft const owners = await this.applicationOwnerService.fetchBySubmissionId(this.submissionUuid); if (owners) { const parcelOwners = owners.filter( - (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) ); this.$owners.next(parcelOwners); } diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts index b6567a7ea4..072389f93b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts @@ -3,12 +3,9 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MatDialog } from '@angular/material/dialog'; import { BehaviorSubject } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; import { ApplicationParcelDto, @@ -16,6 +13,8 @@ import { } from '../../../../../services/application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; import { ParcelService } from '../../../../../services/parcel/parcel.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; @@ -334,9 +333,9 @@ export class ParcelEntryComponent implements OnInit { return owners .filter((owner) => { if (this.isCrownLand) { - return [APPLICATION_OWNER.CROWN].includes(owner.type.code); + return [OWNER_TYPE.CROWN].includes(owner.type.code); } else { - return [APPLICATION_OWNER.INDIVIDUAL, APPLICATION_OWNER.ORGANIZATION].includes(owner.type.code); + return [OWNER_TYPE.INDIVIDUAL, OWNER_TYPE.ORGANIZATION].includes(owner.type.code); } }) .map((owner) => { diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts index 760e03f11a..e0f04f417f 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts @@ -1,8 +1,9 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { ApplicationOwnerDto, APPLICATION_OWNER } from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; import { ApplicationCrownOwnerDialogComponent } from '../application-crown-owner-dialog/application-crown-owner-dialog.component'; import { ApplicationOwnerDialogComponent } from '../application-owner-dialog/application-owner-dialog.component'; @@ -44,7 +45,7 @@ export class ParcelOwnersComponent { onEdit(owner: ApplicationOwnerDto) { let dialog; - if (owner.type.code === APPLICATION_OWNER.CROWN) { + if (owner.type.code === OWNER_TYPE.CROWN) { dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { data: { isDraft: this.isDraft, diff --git a/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts index cc5f41607d..e42b200f13 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts @@ -3,12 +3,14 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { AuthenticationService } from '../../../../services/authentication/authentication.service'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; import { EditApplicationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; @@ -102,10 +104,9 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni ...owner, isSelected: owner.uuid === uuid, })); - this.selectedThirdPartyAgent = - (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.AGENT) || uuid == 'agent'; + this.selectedThirdPartyAgent = (selectedOwner && selectedOwner.type.code === OWNER_TYPE.AGENT) || uuid == 'agent'; this.selectedLocalGovernment = - (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.GOVERNMENT) || uuid == 'government'; + (selectedOwner && selectedOwner.type.code === OWNER_TYPE.GOVERNMENT) || uuid == 'government'; this.form.reset(); if (this.selectedLocalGovernment) { @@ -136,7 +137,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni phoneNumber: selectedOwner.phoneNumber, email: selectedOwner.email, }); - this.isCrownOwner = selectedOwner.type.code === APPLICATION_OWNER.CROWN; + this.isCrownOwner = selectedOwner.type.code === OWNER_TYPE.CROWN; } } this.calculateLetterRequired(); @@ -146,14 +147,14 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni if (this.selectedLocalGovernment) { this.needsAuthorizationLetter = false; } else { - const isSelfApplicant = this.owners.length > 0 && this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL; + const isSelfApplicant = this.owners.length > 0 && this.owners[0].type.code === OWNER_TYPE.INDIVIDUAL; this.needsAuthorizationLetter = this.selectedThirdPartyAgent || !( isSelfApplicant && (this.owners.length === 1 || (this.owners.length === 2 && - this.owners[1].type.code === APPLICATION_OWNER.AGENT && + this.owners[1].type.code === OWNER_TYPE.AGENT && !this.selectedThirdPartyAgent)) ); } @@ -181,7 +182,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni email: this.email.getRawValue() ?? '', phoneNumber: this.phoneNumber.getRawValue() ?? '', ownerUuid: selectedOwner?.uuid, - type: this.selectedThirdPartyAgent ? APPLICATION_OWNER.AGENT : APPLICATION_OWNER.GOVERNMENT, + type: this.selectedThirdPartyAgent ? OWNER_TYPE.AGENT : OWNER_TYPE.GOVERNMENT, }); } else if (selectedOwner) { await this.applicationOwnerService.setPrimaryContact({ @@ -203,13 +204,13 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni if (owners) { const selectedOwner = owners.find((owner) => owner.uuid === primaryContactOwnerUuid); this.parcelOwners = owners.filter( - (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) ); this.owners = owners; if (selectedOwner) { - this.selectedThirdPartyAgent = selectedOwner.type.code === APPLICATION_OWNER.AGENT; - this.selectedLocalGovernment = selectedOwner.type.code === APPLICATION_OWNER.GOVERNMENT; + this.selectedThirdPartyAgent = selectedOwner.type.code === OWNER_TYPE.AGENT; + this.selectedLocalGovernment = selectedOwner.type.code === OWNER_TYPE.GOVERNMENT; } if (this.selectedLocalGovernment) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts index 638e8569b5..6f50c7aa0b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts @@ -3,13 +3,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts index f56da91fb4..c4b34405ef 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts @@ -1,6 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { AuthenticationService } from '../../../../../services/authentication/authentication.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { FilesStepComponent } from '../../files-step.partial'; @@ -8,10 +9,7 @@ import { ApplicationSubmissionService } from '../../../../../services/applicatio import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { MatDialog } from '@angular/material/dialog'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { EditApplicationSteps } from '../../edit-submission.component'; import { takeUntil } from 'rxjs'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts index 7a5a8ff95b..7c9c17c3a4 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts @@ -4,10 +4,7 @@ import { MatDialog } from '@angular/material/dialog'; import { MatRadioChange } from '@angular/material/radio'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto, @@ -15,6 +12,7 @@ import { } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { CodeService } from '../../../../../services/code/code.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToYesNoString } from '../../../../../shared/utils/boolean-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts index 07a2f4552d..f657a6f1aa 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts @@ -3,13 +3,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { MOBILE_BREAKPOINT } from '../../../../../shared/utils/breakpoints'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts index b3ccbb4b96..6f06250f2c 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts @@ -3,13 +3,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts index 83812714ac..3528028be2 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts @@ -3,13 +3,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts index e1bfec8990..9410136d51 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts @@ -4,15 +4,13 @@ import { MatDialog } from '@angular/material/dialog'; import { MatTableDataSource } from '@angular/material/table'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { PARCEL_TYPE } from '../../../../../services/application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts index 0c85764f3e..4a845b4dc0 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts @@ -3,13 +3,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts index 8e8b0f4f06..2ab11d4d1d 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts @@ -1,13 +1,10 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; import { FileHandle } from '../../../../shared/file-drag-drop/drag-drop.directive'; import { ReviewApplicationFngSteps, ReviewApplicationSteps } from '../review-submission.component'; diff --git a/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts index b729afc8d9..fc165fe3c2 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts @@ -2,13 +2,14 @@ import { Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output import { MatExpansionPanel } from '@angular/material/expansion'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ApplicationSubmissionDto } from '../../../../services/application-submission/application-submission.dto'; import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; import { CustomStepperComponent } from '../../../../shared/custom-stepper/custom-stepper.component'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; import { ReviewApplicationFngSteps } from '../review-submission.component'; diff --git a/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts index b14ca7b5f0..0ee3796c66 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts @@ -2,13 +2,14 @@ import { Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output import { MatExpansionPanel } from '@angular/material/expansion'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ApplicationSubmissionDto } from '../../../../services/application-submission/application-submission.dto'; import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; import { CustomStepperComponent } from '../../../../shared/custom-stepper/custom-stepper.component'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; import { ReviewApplicationSteps } from '../review-submission.component'; diff --git a/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts index 91d1d69272..e80935380e 100644 --- a/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts @@ -1,11 +1,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; @@ -14,6 +10,7 @@ import { SUBMISSION_STATUS, } from '../../../../services/application-submission/application-submission.dto'; import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-lfng-review', diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts new file mode 100644 index 0000000000..744f8fb3ee --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts @@ -0,0 +1,48 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatOptionModule } from '@angular/material/core'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTableModule } from '@angular/material/table'; +import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; +import { SharedModule } from '../../../shared/shared.module'; +import { EditSubmissionComponent } from './edit-submission.component'; +import { DeleteParcelDialogComponent } from './parcels/delete-parcel/delete-parcel-dialog.component'; +import { ParcelDetailsComponent } from './parcels/parcel-details.component'; +import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; +import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; +import { ParcelOwnersComponent } from './parcels/parcel-owners/parcel-owners.component'; + +@NgModule({ + declarations: [ + EditSubmissionComponent, + ParcelDetailsComponent, + ParcelEntryComponent, + ParcelEntryConfirmationDialogComponent, + DeleteParcelDialogComponent, + ParcelOwnersComponent, + ], + imports: [ + CommonModule, + SharedModule, + NgxMaskDirective, + NgxMaskPipe, + MatInputModule, + MatButtonToggleModule, + MatFormFieldModule, + MatOptionModule, + MatSelectModule, + MatTableModule, + ], + exports: [ + EditSubmissionComponent, + ParcelDetailsComponent, + ParcelEntryComponent, + ParcelEntryConfirmationDialogComponent, + DeleteParcelDialogComponent, + ParcelOwnersComponent, + ], +}) +export class EditSubmissionBaseModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html index 2d6797f88a..ab2679138c 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html @@ -1 +1,70 @@ -

edit-submission works!

+ +
+
Notice of Intent ID: {{ noiSubmission.fileNumber }} | {{ noiSubmission.type }}
+
+ +
+ + + help_outline + +
+
+
+
+ + + + +
+ +
+
+ + +
Primary Contact
+
+ + +
Select Government
+
+ + +
Land Use
+
+ + +
Proposal
+
+ + +
Other attachments
+
+ + +
Review and Submit
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss index e69de29bb2..e745545965 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss @@ -0,0 +1,378 @@ +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; + +.header { + margin: rem(24) 0; + display: flex; + justify-content: space-between; + flex-flow: row wrap; + + h6 { + display: flex; + align-items: center; + justify-content: center; + } + .header-btn-wrapper { + display: flex; + align-items: center; + flex-wrap: wrap; + margin-top: rem(16); + width: 100%; + + button { + width: 100%; + } + } + + .change-app-type-btn-wrapper { + display: flex; + align-items: center; + margin-top: rem(16); + margin-left: 0; + width: 100%; + + button { + width: 100%; + } + + .mat-icon { + margin-left: rem(8); + width: rem(22); + height: rem(22); + font-size: rem(22); + } + } + + @media screen and (min-width: $tabletBreakpoint) { + .header-btn-wrapper { + margin-top: 0; + width: unset; + + button { + width: auto; + } + + .change-app-type-btn-wrapper { + width: auto; + display: flex; + align-items: center; + margin-top: 0; + margin-left: rem(16); + + button { + width: auto; + } + + .mat-icon { + margin-left: rem(16); + font-size: rem(24); + } + } + } + } +} + +:host::ng-deep { + .step-description { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: rem(24); + margin-bottom: rem(32); + } + + .step-documents { + border: rem(1) solid colors.$secondary-color; + border-radius: rem(4); + padding: rem(8); + + h6 { + color: colors.$secondary-color; + margin-top: unset !important; + } + } + + .mat-icon.mat-icon-inline { + line-height: initial; + } + + .mat-horizontal-content-container { + padding: 0; + min-height: 100%; + } + + .mat-horizontal-stepper-wrapper { + min-height: 100%; + } + + .subtext { + margin-bottom: rem(8) !important; + } + + .mat-step-header .mat-step-icon-selected { + color: #fff; + } + + .mat-step-header .mat-step-label.mat-step-label-active { + display: none; + } + + .mat-step-label { + display: none; + } + + .section { + margin: rem(24) 0; + } + + .date-picker { + ::ng-deep.mat-form-field-infix { + display: flex; + padding: rem(3) 0; + } + } + + .mat-button-wrapper { + white-space: break-spaces; + } + + .parcel-buttons-wrappers button { + margin-top: 1.2rem !important; + } + + @media screen and (min-width: $tabletBreakpoint) { + .mat-step-header .mat-step-label.mat-step-label-active { + display: inherit; + } + + .mat-step-label { + display: inherit; + } + } + + .review-step { + min-height: 100%; + } + + .mobile-hidden { + display: none !important; + + @media screen and (min-width: $tabletBreakpoint) { + display: initial !important; + } + } + + .tablet-hidden { + display: initial !important; + + @media screen and (min-width: $tabletBreakpoint) { + display: none !important; + } + } + + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: rem(33); + } + + .mat-button-toggle-checked.mat-button-toggle-appearance-standard { + background-color: colors.$primary-color-light; + } + + .edit-application { + .description { + margin-top: rem(12); + margin-bottom: rem(20); + display: flex; + flex-direction: column; + + div { + display: flex; + align-items: center; + } + + .save-button { + margin-top: rem(16) !important; + width: 100%; + } + + @media screen and (min-width: $tabletBreakpoint) { + flex-direction: row; + justify-content: space-between; + + .save-button { + margin-left: rem(12) !important; + margin-top: unset !important; + width: unset; + } + } + } + + .button-container { + margin-top: rem(40); + margin-bottom: rem(24); + display: flex; + flex-direction: column-reverse; + justify-content: space-between; + + @media screen and (min-width: $tabletBreakpoint) { + flex-direction: row; + justify-content: space-between; + } + + div { + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: rem(8); + + @media screen and (min-width: $tabletBreakpoint) { + display: flex; + justify-content: space-between; + } + + button { + margin-bottom: rem(24) !important; + + @media screen and (min-width: $tabletBreakpoint) { + margin-left: rem(24) !important; + margin-bottom: 0 !important; + } + } + } + } + } + + // parcel entry details + .float-right { + float: right; + } + + .container { + margin: rem(20) 0 rem(20) 0; + } + + .parcel-checkbox { + margin: rem(20) 0 0 0; + } + + .type { + margin-bottom: rem(30); + } + + .pmbc-search { + border: 1px solid colors.$primary-color-dark; + border-radius: rem(4); + background-color: rgba(colors.$accent-color-light, 0.2); + padding: rem(16); + } + + .field-error { + color: colors.$error-color; + font-size: rem(15); + font-weight: 700; + display: flex; + align-items: center; + margin-top: rem(4); + } + + .form-row { + margin-top: rem(16); + display: grid; + grid-template-columns: 1fr; + grid-column-gap: rem(30); + + @media screen and (min-width: $desktopBreakpoint) { + & { + grid-template-columns: 1fr 1fr; + } + + .full-row { + grid-column: 1/3; + } + } + } + + .radio-row { + margin: rem(10) 0; + display: block; + + .mat-radio-button { + margin-right: rem(12); + } + } + + .full-width-input { + width: 100%; + } + + .mat-expansion-panel { + margin-bottom: rem(16); + } + + .mat-checkbox { + display: flex; + justify-content: center; + .mat-checkbox-layout { + white-space: break-spaces; + } + } + + .flex-center-wrap { + display: flex; + justify-content: center; + flex-wrap: wrap; + } + + .flex-evenly-wrap { + display: flex; + justify-content: space-evenly; + flex-wrap: wrap; + } + + .flex-space-between-wrap { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + } + + .other-parcels-form { + width: 100%; + display: flex; + flex-direction: column; + row-gap: rem(24); + } + + .owner-table-wrapper { + display: block; + overflow: auto; + width: 100%; + } + + .land-use-form { + display: grid; + grid-template-columns: 1fr; + + .land-use-type-wrapper { + display: flex; + gap: rem(24); + flex-wrap: wrap; + width: 100%; + + .land-use-type { + width: 100%; + } + } + + @media screen and (min-width: $tabletBreakpoint) { + .land-use-type-wrapper { + flex-wrap: nowrap; + + .land-use-type { + width: 64%; + } + } + } + } + + .display-block { + display: block; + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts index 74776e7f67..b77ae01080 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts @@ -1,4 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute } from '@angular/router'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { ToastService } from '../../../services/toast/toast.service'; import { EditSubmissionComponent } from './edit-submission.component'; @@ -8,9 +12,26 @@ describe('EditSubmissionComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ EditSubmissionComponent ] - }) - .compileComponents(); + declarations: [EditSubmissionComponent], + providers: [ + { + provide: NoticeOfIntentSubmissionService, + useValue: {}, + }, + { + provide: ToastService, + useValue: {}, + }, + { + provide: ActivatedRoute, + useValue: {}, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + }).compileComponents(); fixture = TestBed.createComponent(EditSubmissionComponent); component = fixture.componentInstance; diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts index 2dcc57f5ab..0f183800a7 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts @@ -1,10 +1,154 @@ -import { Component } from '@angular/core'; +import { StepperSelectionEvent } from '@angular/cdk/stepper'; +import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { ToastService } from '../../../services/toast/toast.service'; +import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; +import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; +import { scrollToElement } from '../../../shared/utils/scroll-helper'; +import { EditApplicationSteps } from '../../applications/edit-submission/edit-submission.component'; +import { ParcelDetailsComponent } from './parcels/parcel-details.component'; + +export enum EditNoiSteps { + Parcel = 0, + PrimaryContact = 1, + Government = 2, + LandUse = 3, + Proposal = 4, + ExtraInfo = 5, + Attachments = 6, + ReviewAndSubmit = 7, +} @Component({ selector: 'app-edit-submission', templateUrl: './edit-submission.component.html', - styleUrls: ['./edit-submission.component.scss'] + styleUrls: ['./edit-submission.component.scss'], }) -export class EditSubmissionComponent { +export class EditSubmissionComponent implements OnDestroy, AfterViewInit { + fileId = ''; + + $destroy = new Subject(); + $noiSubmission = new BehaviorSubject(undefined); + noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; + + steps = EditNoiSteps; + expandedParcelUuid?: string; + showValidationErrors = false; + + @ViewChild('cdkStepper') public customStepper!: CustomStepperComponent; + + @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; + + constructor( + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private activatedRoute: ActivatedRoute, + private dialog: MatDialog, + private toastService: ToastService, + private overlayService: OverlaySpinnerService, + private router: Router + ) { + this.expandedParcelUuid = undefined; + + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + this.noiSubmission = submission; + }); + } + + ngAfterViewInit(): void { + combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.paramMap]) + .pipe(takeUntil(this.$destroy)) + .subscribe(([queryParamMap, paramMap]) => { + const fileId = paramMap.get('fileId'); + if (fileId) { + this.loadSubmission(fileId).then(() => { + const stepInd = paramMap.get('stepInd'); + const parcelUuid = queryParamMap.get('parcelUuid'); + const showErrors = queryParamMap.get('errors'); + if (showErrors) { + this.showValidationErrors = showErrors === 't'; + } + + if (stepInd) { + // setTimeout is required for stepper to be initialized + setTimeout(() => { + this.customStepper.navigateToStep(parseInt(stepInd), true); + + if (parcelUuid) { + this.expandedParcelUuid = parcelUuid; + } + }); + } + }); + } + }); + } + + async onExit() { + await this.router.navigateByUrl(`/notice-of-intent/${this.fileId}`); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + // this gets fired whenever applicant navigates away from edit page + async canDeactivate(): Promise> { + await this.saveSubmission(this.customStepper.selectedIndex); + + return of(true); + } + + async onStepChange($event: StepperSelectionEvent) { + // scrolls to step if step selected programmatically + scrollToElement({ id: `stepWrapper_${$event.selectedIndex}`, center: false }); + } + + async onBeforeSwitchStep(index: number) { + // navigation to url will cause step change based on the index (index starts from 0) + // The save will be triggered using canDeactivate guard + this.showValidationErrors = this.customStepper.selectedIndex === EditNoiSteps.ReviewAndSubmit; + await this.router.navigateByUrl(`notice-of-intent/${this.fileId}/edit/${index}`); + } + + async saveSubmission(step: number) { + switch (step) { + case EditApplicationSteps.AppParcel: + await this.parcelDetailsComponent.onSave(); + break; + default: + this.toastService.showErrorToast('Error updating notice of intent.'); + } + } + + async onDownloadPdf(fileNumber: string | undefined) { + if (fileNumber) { + //TODO: Hook this up later + } + } + + onChangeSubmissionType() { + //TODO: Hook this up later + } + + private async loadSubmission(fileId: string, reload = false) { + if (!this.noiSubmission || reload) { + this.overlayService.showSpinner(); + this.noiSubmission = await this.noticeOfIntentSubmissionService.getByFileId(fileId); + this.fileId = fileId; + this.$noiSubmission.next(this.noiSubmission); + this.overlayService.hideSpinner(); + } + } + onParcelDetailsInitialized() { + if (this.expandedParcelUuid && this.parcelDetailsComponent) { + this.parcelDetailsComponent.openParcel(this.expandedParcelUuid); + this.expandedParcelUuid = undefined; + } + } } diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts index 547fc2c060..ba969b4f33 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts @@ -1,18 +1,26 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Routes } from '@angular/router'; +import { CanDeactivateGuard } from '../../../shared/guard/can-deactivate.guard'; import { SharedModule } from '../../../shared/shared.module'; +import { EditSubmissionBaseModule } from './edit-submission-base.module'; import { EditSubmissionComponent } from './edit-submission.component'; +import { StepComponent } from './step.partial'; const routes: Routes = [ { path: '', component: EditSubmissionComponent, }, + { + path: ':stepInd', + component: EditSubmissionComponent, + canDeactivate: [CanDeactivateGuard], + }, ]; @NgModule({ - declarations: [EditSubmissionComponent], - imports: [CommonModule, SharedModule, RouterModule.forChild(routes)], + declarations: [StepComponent], + imports: [CommonModule, SharedModule, RouterModule.forChild(routes), EditSubmissionBaseModule], }) export class EditSubmissionModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html new file mode 100644 index 0000000000..77c2809519 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html @@ -0,0 +1,36 @@ + + + + + + + + + + +
+

Delete Parcel #{{ parcelNumber }}

+
+ +
+
+ + Warning: All information relevant to this parcel, including information added in subsequent steps, will be + deleted. + + +
+ +
+
+ +
+
+
Are you sure you want to delete this? This action cannot be undone.
+
+ +
+ +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss new file mode 100644 index 0000000000..1f78db7f1e --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss @@ -0,0 +1,24 @@ +@use '../../../../../../styles/functions' as *; + +.margin-bottom-1 { + margin-bottom: rem(16); +} + +.step-controls { + display: flex; + justify-content: space-between; +} + +.confirm-content { + margin: rem(24) 0; +} + +@media screen and (min-width: $desktopBreakpoint) { + .step-controls { + justify-content: flex-end; + + button { + margin-left: rem(25) !important; + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts new file mode 100644 index 0000000000..9beff5eaf0 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HttpClient } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { DeleteParcelDialogComponent } from './delete-parcel-dialog.component'; + +describe('DeleteParcelDialogComponent', () => { + let component: DeleteParcelDialogComponent; + let fixture: ComponentFixture; + let mockHttpClient: DeepMocked; + let mockApplicationParcelService: DeepMocked; + + beforeEach(async () => { + mockHttpClient = createMock(); + mockApplicationParcelService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [DeleteParcelDialogComponent], + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ApplicationParcelService, + useValue: mockApplicationParcelService, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DeleteParcelDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts new file mode 100644 index 0000000000..f86e0979ab --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts @@ -0,0 +1,52 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; + +export enum ApplicationParcelDeleteStepsEnum { + warning = 0, + confirmation = 1, +} + +@Component({ + selector: 'app-delete-parcel-dialog', + templateUrl: './delete-parcel-dialog.component.html', + styleUrls: ['./delete-parcel-dialog.component.scss'], +}) +export class DeleteParcelDialogComponent { + parcelUuid!: string; + parcelNumber!: string; + + stepIdx = 0; + + warningStep = ApplicationParcelDeleteStepsEnum.warning; + confirmationStep = ApplicationParcelDeleteStepsEnum.confirmation; + + constructor( + private applicationParcelService: ApplicationParcelService, + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DeleteParcelDialogComponent + ) { + this.parcelUuid = data.parcelUuid; + this.parcelNumber = data.parcelNumber; + } + + async next() { + this.stepIdx += 1; + } + + async back() { + this.stepIdx -= 1; + } + + async onCancel(dialogResult: boolean = false) { + this.dialogRef.close(dialogResult); + } + + async onDelete() { + const result = await this.applicationParcelService.deleteMany([this.parcelUuid]); + + if (result) { + this.onCancel(true); + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html new file mode 100644 index 0000000000..0a42627959 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html @@ -0,0 +1,68 @@ +
+
+

Identify Parcels Under Application

+

Provide parcel identification and registered ownership information for each parcel under application.

+

*All fields are required unless stated optional.

+
+
Documents needed for this step:
+
    +
  • Certificate of Title
  • +
  • Corporate Summary (if applicable)
  • +
+
+
+ + + Parcel #{{ parcelInd + 1 }} Details & Owner Information + + + + +
+
+ + +
+
+
+ +
+ +
+ +
+ + +
+
+ +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.spec.ts new file mode 100644 index 0000000000..ef3dea17b9 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.spec.ts @@ -0,0 +1,70 @@ +import { HttpClient } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { NoticeOfIntentOwnerService } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { ParcelDetailsComponent } from './parcel-details.component'; + +describe('ParcelDetailsComponent', () => { + let component: ParcelDetailsComponent; + let fixture: ComponentFixture; + let mockHttpClient: DeepMocked; + let mockNOIParcelService: DeepMocked; + let mockNOIOwnerService: DeepMocked; + let mockToastService: DeepMocked; + let mockMatDialog: DeepMocked; + let noiPipe = new BehaviorSubject(undefined); + + beforeEach(async () => { + mockHttpClient = createMock(); + mockNOIParcelService = createMock(); + mockToastService = createMock(); + mockMatDialog = createMock(); + mockNOIOwnerService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ParcelDetailsComponent], + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: NoticeOfIntentParcelService, + useValue: mockNOIParcelService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockNOIOwnerService, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: MatDialog, + useValue: mockMatDialog, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelDetailsComponent); + component = fixture.componentInstance; + component.$noiSubmission = noiPipe; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts new file mode 100644 index 0000000000..d0352c217a --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts @@ -0,0 +1,173 @@ +import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { BehaviorSubject, takeUntil } from 'rxjs'; +import { ApplicationParcelUpdateDto } from '../../../../services/application-parcel/application-parcel.dto'; +import { NoticeOfIntentOwnerDto } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelDto } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; +import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; +import { EditNoiSteps } from '../edit-submission.component'; +import { StepComponent } from '../step.partial'; +import { DeleteParcelDialogComponent } from './delete-parcel/delete-parcel-dialog.component'; +import { ParcelEntryFormData } from './parcel-entry/parcel-entry.component'; + +@Component({ + selector: 'app-noi-parcel-details', + templateUrl: './parcel-details.component.html', + styleUrls: ['./parcel-details.component.scss'], +}) +export class ParcelDetailsComponent extends StepComponent implements OnInit, AfterViewInit { + @Output() componentInitialized = new EventEmitter(); + + currentStep = EditNoiSteps.Parcel; + fileId = ''; + submissionUuid = ''; + parcels: NoticeOfIntentParcelDto[] = []; + $owners = new BehaviorSubject([]); + newParcelAdded = false; + isDirty = false; + + constructor( + private router: Router, + private noiParcelService: NoticeOfIntentParcelService, + private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + private toastService: ToastService, + private dialog: MatDialog + ) { + super(); + } + + ngOnInit(): void { + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + if (noiSubmission) { + this.fileId = noiSubmission.fileNumber; + this.submissionUuid = noiSubmission.uuid; + this.loadParcels(); + const parcelOwners = noiSubmission.owners.filter( + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) + ); + this.$owners.next(parcelOwners); + } + }); + + this.newParcelAdded = false; + } + + ngAfterViewInit(): void { + setTimeout((_) => this.componentInitialized.emit(true)); + } + + async loadParcels() { + this.parcels = (await this.noiParcelService.fetchBySubmissionUuid(this.submissionUuid)) || []; + if (!this.parcels || this.parcels.length === 0) { + await this.onAddParcel(); + } + } + + async onAddParcel() { + const parcel = await this.noiParcelService.create(this.submissionUuid); + + if (parcel) { + this.parcels.push({ + uuid: parcel!.uuid, + owners: [], + isConfirmedByApplicant: false, + }); + this.newParcelAdded = true; + } else { + this.toastService.showErrorToast('Error adding new parcel. Please refresh page and try again.'); + } + } + + async onParcelFormChange(formData: Partial) { + const parcel = this.parcels.find((e) => e.uuid === formData.uuid); + if (!parcel) { + this.toastService.showErrorToast('Error updating the parcel. Please refresh page and try again.'); + return; + } + + this.isDirty = true; + parcel.pid = formData.pid !== undefined ? formData.pid : parcel.pid; + parcel.pin = formData.pid !== undefined ? formData.pin : parcel.pin; + parcel.civicAddress = formData.civicAddress !== undefined ? formData.civicAddress : parcel.civicAddress; + parcel.legalDescription = + formData.legalDescription !== undefined ? formData.legalDescription : parcel.legalDescription; + + parcel.mapAreaHectares = formData.mapArea !== undefined ? formData.mapArea : parcel.mapAreaHectares; + parcel.ownershipTypeCode = formData.parcelType !== undefined ? formData.parcelType : parcel.ownershipTypeCode; + parcel.isFarm = formData.isFarm !== undefined ? parseStringToBoolean(formData.isFarm) : parcel.isFarm; + parcel.purchasedDate = + formData.purchaseDate !== undefined ? formData.purchaseDate?.getTime() : parcel.purchasedDate; + parcel.isConfirmedByApplicant = formData.isConfirmedByApplicant || false; + parcel.crownLandOwnerType = + formData.crownLandOwnerType !== undefined ? formData.crownLandOwnerType : parcel.crownLandOwnerType; + if (formData.owners) { + //parcel.owners = formData.owners; + } + } + + private async saveProgress() { + if (this.isDirty || this.newParcelAdded) { + const parcelsToUpdate: ApplicationParcelUpdateDto[] = []; + for (const parcel of this.parcels) { + parcelsToUpdate.push({ + uuid: parcel.uuid, + pid: parcel.pid?.toString() || null, + pin: parcel.pin?.toString() || null, + civicAddress: parcel.civicAddress ?? null, + legalDescription: parcel.legalDescription, + isFarm: parcel.isFarm, + purchasedDate: parcel.purchasedDate, + mapAreaHectares: parcel.mapAreaHectares, + ownershipTypeCode: parcel.ownershipTypeCode, + isConfirmedByApplicant: parcel.isConfirmedByApplicant, + crownLandOwnerType: parcel.crownLandOwnerType, + ownerUuids: [], + }); + } + await this.noiParcelService.update(parcelsToUpdate); + } + } + + async onSave() { + await this.saveProgress(); + } + + async onDelete(parcelUuid: string, parcelNumber: number) { + this.dialog + .open(DeleteParcelDialogComponent, { + panelClass: 'no-padding', + disableClose: true, + data: { + parcelUuid, + parcelNumber, + }, + }) + .beforeClosed() + .subscribe((result) => { + if (result) { + this.loadParcels(); + } + }); + } + + async onOwnersUpdated() { + const owners = await this.noticeOfIntentOwnerService.fetchBySubmissionId(this.submissionUuid); + if (owners) { + const parcelOwners = owners.filter( + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) + ); + this.$owners.next(parcelOwners); + } + } + + expandedParcel: string = ''; + + openParcel(index: string) { + this.expandedParcel = index; + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html new file mode 100644 index 0000000000..e7e96cb4c5 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html @@ -0,0 +1,35 @@ + + + + + + + + + + +
+

Change Parcel Ownership Type

+
+ +
+
+ + Warning: Changing parcel ownership type will remove some inputs relevant to the current parcel. + + +
+ +
+
+ +
+
+
Are you sure you want to change parcel ownership type? This action cannot be undone.
+
+ +
+ +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss new file mode 100644 index 0000000000..c4e03e5c59 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss @@ -0,0 +1,24 @@ +@use '../../../../../../../styles/functions' as *; + +.margin-bottom-1 { + margin-bottom: rem(16); +} + +.step-controls { + display: flex; + justify-content: space-between; +} + +.confirm-content { + margin: rem(24) 0; +} + +@media screen and (min-width: $desktopBreakpoint) { + .step-controls { + justify-content: flex-end; + + button { + margin-left: rem(25) !important; + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts new file mode 100644 index 0000000000..705f0c459e --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog.component'; + +describe('ParcelEntryConfirmationDialogComponent', () => { + let component: ParcelEntryConfirmationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ParcelEntryConfirmationDialogComponent], + providers: [ + { + provide: MatDialog, + useValue: {}, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelEntryConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts new file mode 100644 index 0000000000..dfe4bb5e16 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { ApplicationParcelDeleteStepsEnum } from '../../delete-parcel/delete-parcel-dialog.component'; + +@Component({ + selector: 'app-parcel-entry-confirmation-dialog', + templateUrl: './parcel-entry-confirmation-dialog.component.html', + styleUrls: ['./parcel-entry-confirmation-dialog.component.scss'], +}) +export class ParcelEntryConfirmationDialogComponent { + stepIdx = 0; + + warningStep = ApplicationParcelDeleteStepsEnum.warning; + confirmationStep = ApplicationParcelDeleteStepsEnum.confirmation; + + constructor(private dialogRef: MatDialogRef) {} + + async next() { + this.stepIdx += 1; + } + + async back() { + this.stepIdx -= 1; + } + + async onCancel(dialogResult: boolean = false) { + this.dialogRef.close(dialogResult); + } + + async onDelete() { + this.onCancel(true); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html new file mode 100644 index 0000000000..ad624d34bd --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html @@ -0,0 +1,372 @@ +
+
+ +
+ The answer to the following question will change the rest of the application form. Do not change this answer once + selected. +
+ + Fee Simple + Crown + +
+ warning +
This field is required
+
+
+ +
Parcel Lookup
+
+ + +
+ +
Can be found on the parcel's Certificate of Title
+ + + +
+ warning +
This field is required
+
+
+ +
+ +
The area of the entire parcel in hectares, not just the area under application.
+ + + +
+ warning +
This field is required
+
+
+ +
+ + + + +
+ warning +
This field is required
+
Invalid format
+
+
+ +
+ + + + +
+ +
+ + + + + + +
+ warning +
This field is required
+
+
+ Example: 2020-Mar-01 +
+
+ +
+ +
+ As determined by + BC Assessment +
+ + Yes + No + +
+ warning +
This field is required
+
+
+ +
+ + + + +
+ warning +
This field is required
+
+
+
+ +
+ + +
+ +
+
Owner Information
+ +

Provide the following information for all owners listed on the parcel's Certificate of Title

+ + + + + +
+
{{ option.displayName }}
+ +
+
+ +
+
No owner matching search
+ +
+
+ + See all Owners + +
+
+
+ warning +
This field is required
+
+ +
+ +
+
+ +
+ +
+ + Provincial Crown + Federal Crown +
+ warning +
This field is required
+
+
+ + + + +
+
{{ option.displayName }}
+ +
+
+ +
+
No owner matching search
+ +
+
+ + See all Owners + +
+
+
+ warning +
This field is required
+
+ +
+ + + + + + + + + + +
+
+
+ I confirm that the owner information provided above matches the current Certificate of Title. Mismatched + information can cause significant delays to processing time. + +
+ warning +
This field is required
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.scss new file mode 100644 index 0000000000..223b120fa2 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.scss @@ -0,0 +1,96 @@ +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; + +.owner-option { + display: flex; + justify-content: space-between; + align-items: center; +} + +.new-owner { + margin: rem(8) 0 !important; +} + +.link-text { + color: colors.$link-color; + text-decoration: underline; +} + +.lookup-pid-fields { + display: flex; + align-items: center; + flex-direction: column; + + @media screen and (min-width: $desktopBreakpoint) { + flex-direction: row; + } +} + +.lookup-search-by { + margin-top: rem(8); + + @media screen and (min-width: $desktopBreakpoint) { + width: unset !important; + flex-grow: 1; + } +} + +.lookup-input { + margin-top: rem(8); + + @media screen and (min-width: $desktopBreakpoint) { + width: unset !important; + flex-grow: 5; + } +} + +.lookup-search-button { + width: 100%; + margin-top: rem(8) !important; + + @media screen and (min-width: $desktopBreakpoint) { + height: rem(55); + margin-top: rem(7) !important; + width: rem(150); + } +} + +.lookup-bottom-row { + display: flex; + align-items: center; + flex-direction: column; + margin-top: rem(24); + + .reset-button { + width: 100%; + margin-bottom: rem(8); + } + + @media screen and (min-width: $desktopBreakpoint) { + flex-direction: row; + justify-content: space-between; + + .reset-button { + width: unset; + } + } +} + +.crown-owner-type { + display: block; + margin-bottom: rem(24); + + .mat-mdc-radio-button ~ .mat-mdc-radio-button { + margin-left: rem(16); + } +} + +mat-button-toggle-group#isFarm { + height: rem(55); + + .mat-button-toggle { + display: flex; + align-items: center; + height: 100%; + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts new file mode 100644 index 0000000000..9bce91efb1 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts @@ -0,0 +1,81 @@ +import { HttpClient } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelDto } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { ParcelService } from '../../../../../services/parcel/parcel.service'; +import { ParcelEntryComponent } from './parcel-entry.component'; + +describe('ParcelEntryComponent', () => { + let component: ParcelEntryComponent; + let fixture: ComponentFixture; + let mockParcelService: DeepMocked; + let mockHttpClient: DeepMocked; + let mockNOIParcelService: DeepMocked; + let mockNOIOwnerService: DeepMocked; + let mockNOIDocumentService: DeepMocked; + + let mockParcel: NoticeOfIntentParcelDto = { + isConfirmedByApplicant: false, + uuid: '', + owners: [], + }; + + beforeEach(async () => { + mockParcelService = createMock(); + mockHttpClient = createMock(); + mockNOIParcelService = createMock(); + mockNOIOwnerService = createMock(); + mockNOIDocumentService = createMock(); + + await TestBed.configureTestingModule({ + imports: [MatAutocompleteModule], + declarations: [ParcelEntryComponent], + providers: [ + { + provide: ParcelService, + useValue: mockParcelService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: NoticeOfIntentParcelService, + useValue: mockNOIParcelService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockNOIOwnerService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNOIDocumentService, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelEntryComponent); + component = fixture.componentInstance; + component.$owners = new BehaviorSubject([]); + component.parcel = mockParcel; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts new file mode 100644 index 0000000000..bb0f985ece --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts @@ -0,0 +1,501 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject } from 'rxjs'; +import { PARCEL_OWNERSHIP_TYPE } from '../../../../../services/application-parcel/application-parcel.dto'; +import { NoticeOfIntentDocumentDto } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelDto } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { ParcelService } from '../../../../../services/parcel/parcel.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; +import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; +import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; +import { RemoveFileConfirmationDialogComponent } from '../../../../applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; +import { ApplicationCrownOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component'; +import { ApplicationOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component'; +import { ApplicationOwnersDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component'; +import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; + +export interface ParcelEntryFormData { + uuid: string; + legalDescription: string | undefined | null; + mapArea: string | undefined | null; + pin: string | undefined | null; + pid: string | undefined | null; + civicAddress: string | undefined | null; + parcelType: string | undefined | null; + isFarm: string | undefined | null; + purchaseDate?: Date | null; + crownLandOwnerType?: string | null; + isConfirmedByApplicant: boolean; + owners: NoticeOfIntentOwnerDto[]; +} + +@Component({ + selector: 'app-noi-parcel-entry[parcel][fileId][submissionUuid]', + templateUrl: './parcel-entry.component.html', + styleUrls: ['./parcel-entry.component.scss'], +}) +export class ParcelEntryComponent implements OnInit { + @Input() parcel!: NoticeOfIntentParcelDto; + @Input() fileId!: string; + @Input() submissionUuid!: string; + @Input() $owners: BehaviorSubject = new BehaviorSubject([]); + + @Input() enableOwners = true; + @Input() enableCertificateOfTitleUpload = true; + @Input() enableUserSignOff = true; + @Input() enableAddNewOwner = true; + @Input() showErrors = false; + @Input() _disabled = false; + @Input() isDraft = false; + + @Input() + public set disabled(disabled: boolean) { + this._disabled = disabled; + this.onFormDisabled(); + } + + @Output() private onFormGroupChange = new EventEmitter>(); + @Output() private onSaveProgress = new EventEmitter(); + @Output() onOwnersUpdated = new EventEmitter(); + + owners: NoticeOfIntentOwnerDto[] = []; + filteredOwners: (NoticeOfIntentOwnerDto & { isSelected: boolean })[] = []; + + searchBy = new FormControl(null); + isCrownLand: boolean | null = null; + isCertificateOfTitleRequired = true; + + pidPin = new FormControl(''); + legalDescription = new FormControl(null, [Validators.required]); + mapArea = new FormControl(null, [Validators.required]); + pid = new FormControl(null, [Validators.required]); + pin = new FormControl(null); + civicAddress = new FormControl(null, [Validators.required]); + parcelType = new FormControl(null, [Validators.required]); + isFarm = new FormControl(null, [Validators.required]); + purchaseDate = new FormControl(null, [Validators.required]); + crownLandOwnerType = new FormControl(null); + isConfirmedByApplicant = new FormControl(false, [Validators.requiredTrue]); + parcelForm = new FormGroup({ + pidPin: this.pidPin, + legalDescription: this.legalDescription, + mapArea: this.mapArea, + pin: this.pin, + pid: this.pid, + civicAddress: this.civicAddress, + parcelType: this.parcelType, + isFarm: this.isFarm, + purchaseDate: this.purchaseDate, + crownLandOwnerType: this.crownLandOwnerType, + isConfirmedByApplicant: this.isConfirmedByApplicant, + searchBy: this.searchBy, + }); + pidPinPlaceholder = ''; + + ownerInput = new FormControl(null); + + DOCUMENT_TYPES = DOCUMENT_TYPE; + PARCEL_OWNERSHIP_TYPES = PARCEL_OWNERSHIP_TYPE; + maxPurchasedDate = new Date(); + + constructor( + private parcelService: ParcelService, + private noticeOfIntentParcelService: NoticeOfIntentParcelService, + private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + private dialog: MatDialog + ) {} + + ngOnInit(): void { + this.setupForm(); + + this.$owners.subscribe((owners) => { + this.owners = owners; + this.filteredOwners = this.mapOwners(owners); + this.parcel.owners = this.parcel.owners.map((owner) => { + const updatedOwner = owners.find((updatedOwner) => updatedOwner.uuid === owner.uuid)!; + if (!updatedOwner) { + console.warn('Failed to find user in array'); + return owner; + } + return updatedOwner; + }); + }); + } + + async onSearch() { + let result; + if (this.searchBy.getRawValue() === 'pin') { + result = await this.parcelService.getByPin(this.pidPin.getRawValue()!); + } else { + result = await this.parcelService.getByPid(this.pidPin.getRawValue()!); + } + + this.onReset(); + if (result) { + this.legalDescription.setValue(result.legalDescription); + this.mapArea.setValue(result.mapArea); + + if (result.pin) { + this.pin.setValue(result.pin); + } + + if (result.pid) { + this.pid.setValue(result.pid); + } + + this.emitFormChangeOnSearchActions(); + } + } + + onReset() { + this.parcelForm.controls.pidPin.reset(); + this.parcelForm.controls.pid.reset(); + this.parcelForm.controls.pin.reset(); + this.parcelForm.controls.legalDescription.reset(); + this.parcelForm.controls.mapArea.reset(); + this.parcelForm.controls.purchaseDate.reset(); + this.parcelForm.controls.isFarm.reset(); + this.parcelForm.controls.civicAddress.reset(); + + this.emitFormChangeOnSearchActions(); + + if (this.showErrors) { + this.parcelForm.markAllAsTouched(); + } else { + this.parcelForm.controls.isFarm.markAsTouched(); + } + } + + private emitFormChangeOnSearchActions() { + this.onFormGroupChange.emit({ + uuid: this.parcel.uuid, + legalDescription: this.legalDescription.getRawValue(), + mapArea: this.mapArea.getRawValue(), + pin: this.pin.getRawValue(), + pid: this.pid.getRawValue(), + }); + } + + onChangeParcelType($event: MatButtonToggleChange) { + const dirtyForm = + this.legalDescription.value || + this.mapArea.value || + this.pid.value || + this.pin.value || + this.purchaseDate.value || + this.isFarm.value || + this.civicAddress.value; + + const changeParcelType = () => { + if ($event.value === this.PARCEL_OWNERSHIP_TYPES.CROWN) { + this.searchBy.setValue(null); + this.pidPinPlaceholder = ''; + this.isCrownLand = true; + this.pid.setValidators([]); + this.purchaseDate.disable(); + } else { + this.searchBy.setValue('pid'); + this.pidPinPlaceholder = 'Type 9 digit PID'; + this.isCrownLand = false; + this.pid.setValidators([Validators.required]); + this.crownLandOwnerType.setValue(null); + this.purchaseDate.enable(); + } + + this.updateParcelOwners([]); + this.filteredOwners = this.mapOwners(this.owners); + this.pid.updateValueAndValidity(); + }; + + if (dirtyForm && this.isCrownLand !== null) { + this.dialog + .open(ParcelEntryConfirmationDialogComponent, { + panelClass: 'no-padding', + disableClose: true, + }) + .beforeClosed() + .subscribe(async (result) => { + if (result) { + this.onReset(); + return changeParcelType(); + } else { + const newParcelType = this.parcelType.getRawValue(); + + const prevParcelType = + newParcelType === this.PARCEL_OWNERSHIP_TYPES.CROWN + ? this.PARCEL_OWNERSHIP_TYPES.FEE_SIMPLE + : this.PARCEL_OWNERSHIP_TYPES.CROWN; + + this.parcelType.setValue(prevParcelType); + } + }); + } else { + return changeParcelType(); + } + } + + async attachFile(file: FileHandle, parcelUuid: string) { + if (parcelUuid) { + const mappedFiles = file.file; + this.parcel.certificateOfTitle = await this.noticeOfIntentParcelService.attachCertificateOfTitle( + this.fileId, + parcelUuid, + mappedFiles + ); + } + } + + async deleteFile($event: NoticeOfIntentDocumentDto) { + if (this.isDraft) { + this.dialog + .open(RemoveFileConfirmationDialogComponent) + .beforeClosed() + .subscribe(async (didConfirm) => { + if (didConfirm) { + await this.noticeOfIntentDocumentService.deleteExternalFile($event.uuid); + this.parcel.certificateOfTitle = undefined; + } + }); + } else { + await this.noticeOfIntentDocumentService.deleteExternalFile($event.uuid); + this.parcel.certificateOfTitle = undefined; + } + } + + async openFile(uuid: string) { + const res = await this.noticeOfIntentDocumentService.openFile(uuid); + if (res) { + window.open(res.url, '_blank'); + } + } + + async beforeFileUploadOpened() { + this.onSaveProgress.emit(); + } + + onAddNewOwner() { + const dialog = this.dialog.open(ApplicationOwnerDialogComponent, { + data: { + fileId: this.fileId, + submissionUuid: this.submissionUuid, + parcelUuid: this.parcel.uuid, + }, + }); + dialog.beforeClosed().subscribe((createdDto) => { + if (createdDto) { + this.onOwnersUpdated.emit(); + const updatedArray = [...this.parcel.owners, createdDto]; + this.updateParcelOwners(updatedArray); + } + }); + } + + onAddNewGovernmentContact() { + const dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { + data: { + fileId: this.fileId, + submissionUuid: this.submissionUuid, + parcelUuid: this.parcel.uuid, + }, + }); + dialog.beforeClosed().subscribe((createdDto) => { + if (createdDto) { + this.onOwnersUpdated.emit(); + const updatedArray = [...this.parcel.owners, createdDto]; + this.updateParcelOwners(updatedArray); + } + }); + } + + async onSelectOwner(owner: NoticeOfIntentOwnerDto, isSelected: boolean) { + if (!isSelected) { + const selectedOwners = [...this.parcel.owners, owner]; + this.updateParcelOwners(selectedOwners); + } + } + + async onOwnerRemoved(uuid: string) { + const updatedArray = this.parcel.owners.filter((existingOwner) => existingOwner.uuid !== uuid); + this.updateParcelOwners(updatedArray); + } + + mapOwners(owners: NoticeOfIntentOwnerDto[]) { + return owners + .filter((owner) => { + if (this.isCrownLand) { + return [OWNER_TYPE.CROWN].includes(owner.type.code); + } else { + return [OWNER_TYPE.INDIVIDUAL, OWNER_TYPE.ORGANIZATION].includes(owner.type.code); + } + }) + .map((owner) => { + const isSelected = this.parcel.owners.some((parcelOwner) => parcelOwner.uuid === owner.uuid); + return { + ...owner, + isSelected, + }; + }) + .sort(this.noticeOfIntentOwnerService.sortOwners); + } + + onTypeOwner($event: Event) { + const element = $event.target as HTMLInputElement; + this.filteredOwners = this.mapOwners(this.owners).filter((option) => { + return option.displayName.toLowerCase().includes(element.value.toLowerCase()); + }); + } + + onSeeAllOwners() { + this.dialog + .open(ApplicationOwnersDialogComponent, { + data: { + owners: this.owners, + fileId: this.fileId, + submissionUuid: this.submissionUuid, + }, + }) + .beforeClosed() + .subscribe((isDirty) => { + if (isDirty) { + this.onOwnersUpdated.emit(); + } + }); + } + + private setupForm() { + this.parcelForm.patchValue({ + legalDescription: this.parcel.legalDescription, + mapArea: this.parcel.mapAreaHectares, + pid: this.parcel.pid, + pin: this.parcel.pin, + civicAddress: this.parcel.civicAddress, + parcelType: this.parcel.ownershipTypeCode, + isFarm: formatBooleanToString(this.parcel.isFarm), + purchaseDate: this.parcel.purchasedDate ? new Date(this.parcel.purchasedDate) : null, + crownLandOwnerType: this.parcel.crownLandOwnerType, + isConfirmedByApplicant: this.enableUserSignOff ? this.parcel.isConfirmedByApplicant : false, + }); + + this.isCrownLand = this.parcelType.value + ? this.parcelType.getRawValue() === this.PARCEL_OWNERSHIP_TYPES.CROWN + : null; + + if (this.isCrownLand) { + this.pidPin.disable(); + this.purchaseDate.disable(); + this.pid.setValidators([]); + const pidValue = this.pid.getRawValue(); + this.isCertificateOfTitleRequired = !!pidValue && pidValue.length > 0; + this.pidPinPlaceholder = ''; + } else { + this.pidPinPlaceholder = 'Type 9 digit PID'; + this.isCertificateOfTitleRequired = true; + } + + if (this.parcel.owners.length > 0 && this.isCrownLand) { + this.ownerInput.disable(); + } + + if (this.showErrors) { + this.parcelForm.markAllAsTouched(); + + if (this.parcel.owners.length === 0) { + this.ownerInput.setValidators([Validators.required]); + this.ownerInput.setErrors({ required: true }); + this.ownerInput.markAllAsTouched(); + } + } + + this.parcelForm.valueChanges.subscribe((formData) => { + if (!this.parcelForm.dirty) { + return; + } + + if ((this.isCrownLand && !this.searchBy.getRawValue()) || this.disabled) { + this.pidPin.disable({ + emitEvent: false, + }); + } else { + this.pidPin.enable({ + emitEvent: false, + }); + } + + const pidValue = this.pid.getRawValue(); + if (this.isCrownLand) { + this.isCertificateOfTitleRequired = !!pidValue && pidValue.length > 0; + } else { + this.isCertificateOfTitleRequired = true; + } + + if (this.parcelForm.dirty && formData.isConfirmedByApplicant === this.parcel.isConfirmedByApplicant) { + this.parcel.isConfirmedByApplicant = false; + formData.isConfirmedByApplicant = false; + + this.parcelForm.patchValue( + { + isConfirmedByApplicant: false, + }, + { emitEvent: false } + ); + } + + return this.onFormGroupChange.emit({ + ...formData, + uuid: this.parcel.uuid, + isConfirmedByApplicant: formData.isConfirmedByApplicant!, + purchaseDate: new Date(formData.purchaseDate?.valueOf()), + }); + }); + } + + private updateParcelOwners(updatedArray: NoticeOfIntentOwnerDto[]) { + if (updatedArray.length > 0) { + this.ownerInput.clearValidators(); + this.ownerInput.updateValueAndValidity(); + } else if (updatedArray.length === 0 && this.showErrors) { + this.ownerInput.markAllAsTouched(); + this.ownerInput.setValidators([Validators.required]); + this.ownerInput.setErrors({ required: true }); + } + + if (this.isCrownLand && updatedArray.length > 0) { + this.ownerInput.disable(); + } else { + this.ownerInput.enable(); + } + + this.parcel.owners = updatedArray; + this.filteredOwners = this.mapOwners(this.owners); + this.onFormGroupChange.emit({ + uuid: this.parcel.uuid, + owners: updatedArray, + }); + } + + private onFormDisabled() { + if (this._disabled) { + this.parcelForm.disable(); + this.ownerInput.disable(); + } else { + this.parcelForm.enable(); + this.ownerInput.enable(); + } + } + + onChangeSearchBy(value: string) { + if (value === 'pid') { + this.pidPinPlaceholder = 'Type 9 digit PID'; + } else { + this.pidPinPlaceholder = 'Type PIN'; + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html new file mode 100644 index 0000000000..51ea96511d --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html @@ -0,0 +1,73 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type{{ element.type.label }}{{ element.position }}Full Name{{ element.displayName }}Organization NameOrganization Name / Ministry / Department + + {{ element.organizationName }} + Ministry/ Department{{ element.organizationName }}Phone{{ element.phoneNumber | mask : '(000) 000-0000' }}Email{{ element.email }}Actions + + + +
No owner information
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss new file mode 100644 index 0000000000..fb105e09eb --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss @@ -0,0 +1,14 @@ +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; + +.actions { + button:not(:last-child) { + margin-right: rem(8) !important; + } +} + +.no-data-text { + text-align: center; + color: colors.$grey; + padding-top: rem(12); +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts new file mode 100644 index 0000000000..b36089cde8 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts @@ -0,0 +1,41 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { ParcelOwnersComponent } from './parcel-owners.component'; + +describe('ParcelOwnersComponent', () => { + let component: ParcelOwnersComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + { + provide: MatDialog, + useValue: {}, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: {}, + }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, + ], + declarations: [ParcelOwnersComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelOwnersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts new file mode 100644 index 0000000000..ebfcd50c62 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts @@ -0,0 +1,91 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { NoticeOfIntentOwnerDto } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; +import { ApplicationCrownOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component'; +import { ApplicationOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component'; + +@Component({ + selector: 'app-parcel-owners[owners][fileId][submissionUuid]', + templateUrl: './parcel-owners.component.html', + styleUrls: ['./parcel-owners.component.scss'], +}) +export class ParcelOwnersComponent { + @Output() onOwnersUpdated = new EventEmitter(); + @Output() onOwnerRemoved = new EventEmitter(); + + @Input() + public set owners(owners: NoticeOfIntentOwnerDto[]) { + this._owners = owners.sort(this.noticeOfIntentOwnerService.sortOwners); + } + + @Input() + public set disabled(disabled: boolean) { + this._disabled = disabled; + } + + @Input() submissionUuid!: string; + @Input() fileId!: string; + @Input() parcelUuid?: string | undefined; + @Input() isCrown = false; + @Input() isDraft = false; + @Input() isShowAllOwners = false; + + _owners: NoticeOfIntentOwnerDto[] = []; + _disabled = false; + displayedColumns = ['type', 'position', 'displayName', 'organizationName', 'phone', 'email', 'actions']; + + constructor( + private dialog: MatDialog, + private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + private confDialogService: ConfirmationDialogService + ) {} + + onEdit(owner: NoticeOfIntentOwnerDto) { + let dialog; + if (owner.type.code === OWNER_TYPE.CROWN) { + dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { + data: { + isDraft: this.isDraft, + parcelUuid: this.parcelUuid, + existingOwner: owner, + submissionUuid: this.submissionUuid, + }, + }); + } else { + dialog = this.dialog.open(ApplicationOwnerDialogComponent, { + data: { + isDraft: this.isDraft, + fileId: this.fileId, + submissionUuid: this.submissionUuid, + parcelUuid: this.parcelUuid, + existingOwner: owner, + }, + }); + } + dialog.beforeClosed().subscribe((updatedUuid) => { + if (updatedUuid) { + this.onOwnersUpdated.emit(); + } + }); + } + + async onRemove(uuid: string) { + this.onOwnerRemoved.emit(uuid); + } + + async onDelete(owner: NoticeOfIntentOwnerDto) { + this.confDialogService + .openDialog({ + body: `This action will remove ${owner.displayName} and its usage from the entire application. Are you sure you want to remove this owner? `, + }) + .subscribe(async (didConfirm) => { + if (didConfirm) { + await this.noticeOfIntentOwnerService.delete(owner.uuid); + this.onOwnersUpdated.emit(); + } + }); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/step.partial.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/step.partial.ts new file mode 100644 index 0000000000..6ac94b8e94 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/step.partial.ts @@ -0,0 +1,34 @@ +import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; + +@Component({ + selector: 'app-step', + template: '

', + styleUrls: [], +}) +export class StepComponent implements OnDestroy { + protected $destroy = new Subject(); + + @Input() $noiSubmission!: BehaviorSubject; + + @Input() showErrors = false; + @Input() draftMode = false; + + @Output() navigateToStep = new EventEmitter(); + @Output() exit = new EventEmitter(); + + async onSaveExit() { + this.exit.emit(); + } + + onNavigateToStep(step: number) { + this.navigateToStep.emit(step); + } + + async ngOnDestroy() { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/portal-frontend/src/app/services/application-document/application-document.dto.ts b/portal-frontend/src/app/services/application-document/application-document.dto.ts index f5762d92fe..24a998e007 100644 --- a/portal-frontend/src/app/services/application-document/application-document.dto.ts +++ b/portal-frontend/src/app/services/application-document/application-document.dto.ts @@ -1,47 +1,7 @@ -import { BaseCodeDto } from '../../shared/dto/base.dto'; - -export enum DOCUMENT_TYPE { - //ALCS - DECISION_DOCUMENT = 'DPAC', - OTHER = 'OTHR', - - //Government Review - RESOLUTION_DOCUMENT = 'RESO', - STAFF_REPORT = 'STFF', - - //Applicant Uploaded - CORPORATE_SUMMARY = 'CORS', - PROFESSIONAL_REPORT = 'PROR', - PHOTOGRAPH = 'PHTO', - AUTHORIZATION_LETTER = 'AAGR', - CERTIFICATE_OF_TITLE = 'CERT', - - //App Documents - SERVING_NOTICE = 'POSN', - PROPOSAL_MAP = 'PRSK', - HOMESITE_SEVERANCE = 'HOME', - CROSS_SECTIONS = 'SPCS', - RECLAMATION_PLAN = 'RECP', - NOTICE_OF_WORK = 'NOWE', - PROOF_OF_SIGNAGE = 'POSA', - REPORT_OF_PUBLIC_HEARING = 'ROPH', - PROOF_OF_ADVERTISING = 'POAA', -} - -export enum DOCUMENT_SOURCE { - APPLICANT = 'Applicant', - ALC = 'ALC', - LFNG = 'L/FNG', - AFFECTED_PARTY = 'Affected Party', - PUBLIC = 'Public', -} - -export interface ApplicationDocumentTypeDto extends BaseCodeDto { - code: DOCUMENT_TYPE; -} +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../shared/dto/document.dto'; export interface ApplicationDocumentDto { - type: ApplicationDocumentTypeDto | null; + type: DocumentTypeDto | null; description?: string | null; uuid: string; fileName: string; diff --git a/portal-frontend/src/app/services/application-document/application-document.service.spec.ts b/portal-frontend/src/app/services/application-document/application-document.service.spec.ts index 44f8eaa138..285edba50b 100644 --- a/portal-frontend/src/app/services/application-document/application-document.service.spec.ts +++ b/portal-frontend/src/app/services/application-document/application-document.service.spec.ts @@ -71,7 +71,7 @@ describe('ApplicationDocumentService', () => { expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); }); - it('should make a delete request for update file', async () => { + it('should make a patch request for update file', async () => { mockHttpClient.patch.mockReturnValue(of({})); await service.update('fileId', []); diff --git a/portal-frontend/src/app/services/application-document/application-document.service.ts b/portal-frontend/src/app/services/application-document/application-document.service.ts index 34020c3ebe..41067bf26d 100644 --- a/portal-frontend/src/app/services/application-document/application-document.service.ts +++ b/portal-frontend/src/app/services/application-document/application-document.service.ts @@ -2,15 +2,11 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; import { DocumentService } from '../document/document.service'; import { ToastService } from '../toast/toast.service'; -import { - ApplicationDocumentDto, - ApplicationDocumentUpdateDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from './application-document.dto'; +import { ApplicationDocumentDto, ApplicationDocumentUpdateDto } from './application-document.dto'; @Injectable({ providedIn: 'root', diff --git a/portal-frontend/src/app/services/application-owner/application-owner.dto.ts b/portal-frontend/src/app/services/application-owner/application-owner.dto.ts index 090698c4d0..c37d003247 100644 --- a/portal-frontend/src/app/services/application-owner/application-owner.dto.ts +++ b/portal-frontend/src/app/services/application-owner/application-owner.dto.ts @@ -1,19 +1,8 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; +import { OWNER_TYPE, OwnerTypeDto } from '../../shared/dto/owner.dto'; import { ApplicationDocumentDto } from '../application-document/application-document.dto'; import { ApplicationParcelDto } from '../application-parcel/application-parcel.dto'; -export enum APPLICATION_OWNER { - INDIVIDUAL = 'INDV', - ORGANIZATION = 'ORGZ', - AGENT = 'AGEN', - CROWN = 'CRWN', - GOVERNMENT = 'GOVR', -} - -export interface ApplicationOwnerTypeDto extends BaseCodeDto { - code: APPLICATION_OWNER; -} - export interface ApplicationOwnerDto { uuid: string; applicationSubmissionUuid: string; @@ -23,7 +12,7 @@ export interface ApplicationOwnerDto { organizationName: string | null; phoneNumber: string | null; email: string | null; - type: ApplicationOwnerTypeDto; + type: OwnerTypeDto; corporateSummary?: ApplicationDocumentDto; } @@ -51,7 +40,7 @@ export interface SetPrimaryContactDto { organization?: string; phoneNumber?: string; email?: string; - type?: APPLICATION_OWNER; + type?: OWNER_TYPE; ownerUuid?: string; applicationSubmissionUuid: string; } diff --git a/portal-frontend/src/app/services/application-owner/application-owner.service.ts b/portal-frontend/src/app/services/application-owner/application-owner.service.ts index debb0ae2c4..290e3a83db 100644 --- a/portal-frontend/src/app/services/application-owner/application-owner.service.ts +++ b/portal-frontend/src/app/services/application-owner/application-owner.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../application-document/application-document.dto'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; import { DocumentService } from '../document/document.service'; import { ToastService } from '../toast/toast.service'; import { diff --git a/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts b/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts index 813a830003..1112a20315 100644 --- a/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts +++ b/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts @@ -2,15 +2,12 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; -import { - ApplicationDocumentDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from '../application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../application-document/application-document.dto'; import { DocumentService } from '../document/document.service'; import { ToastService } from '../toast/toast.service'; -import { ApplicationParcelDto, ApplicationParcelUpdateDto, PARCEL_TYPE } from './application-parcel.dto'; +import { ApplicationParcelDto, ApplicationParcelUpdateDto } from './application-parcel.dto'; @Injectable({ providedIn: 'root', @@ -37,12 +34,11 @@ export class ApplicationParcelService { return undefined; } - async create(applicationSubmissionUuid: string, parcelType?: PARCEL_TYPE, ownerUuid?: string) { + async create(applicationSubmissionUuid: string, ownerUuid?: string) { try { return await firstValueFrom( this.httpClient.post(`${this.serviceUrl}`, { applicationSubmissionUuid, - parcelType, ownerUuid, }) ); diff --git a/portal-frontend/src/app/services/code/code.service.ts b/portal-frontend/src/app/services/code/code.service.ts index 9115600fd5..8dc58f9d8c 100644 --- a/portal-frontend/src/app/services/code/code.service.ts +++ b/portal-frontend/src/app/services/code/code.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { ApplicationDocumentTypeDto } from '../application-document/application-document.dto'; +import { DocumentTypeDto } from '../../shared/dto/document.dto'; import { NaruSubtypeDto } from '../application-submission/application-submission.dto'; import { ApplicationTypeDto, LocalGovernmentDto, NoticeOfIntentTypeDto, SubmissionTypeDto } from './code.dto'; @@ -20,7 +20,7 @@ export class CodeService { localGovernments: LocalGovernmentDto[]; applicationTypes: ApplicationTypeDto[]; noticeOfIntentTypes: NoticeOfIntentTypeDto[]; - applicationDocumentTypes: ApplicationDocumentTypeDto[]; + documentTypes: DocumentTypeDto[]; submissionTypes: SubmissionTypeDto[]; naruSubtypes: NaruSubtypeDto[]; }>(`${this.baseUrl}`) diff --git a/portal-frontend/src/app/services/document/document.service.ts b/portal-frontend/src/app/services/document/document.service.ts index e4e82374e8..899968fcbc 100644 --- a/portal-frontend/src/app/services/document/document.service.ts +++ b/portal-frontend/src/app/services/document/document.service.ts @@ -2,8 +2,8 @@ import { HttpBackend, HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../application-document/application-document.dto'; import { ToastService } from '../toast/toast.service'; import { UploadDocumentUrlDto } from './document.dto'; diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.dto.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.dto.ts new file mode 100644 index 0000000000..cd724b639a --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.dto.ts @@ -0,0 +1,18 @@ +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../shared/dto/document.dto'; + +export interface NoticeOfIntentDocumentDto { + type: DocumentTypeDto | null; + description?: string | null; + uuid: string; + fileName: string; + fileSize: number; + uploadedBy: string; + uploadedAt: number; + source: DOCUMENT_SOURCE; +} + +export interface NoticeOfIntentDocumentUpdateDto { + uuid: string; + type: DOCUMENT_TYPE | null; + description?: string | null; +} diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts new file mode 100644 index 0000000000..5c07e25903 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts @@ -0,0 +1,91 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../toast/toast.service'; +import { NoticeOfIntentDocumentService } from './notice-of-intent-document.service'; + +describe('NoticeOfIntentDocumentService', () => { + let service: NoticeOfIntentDocumentService; + let mockToastService: DeepMocked; + let mockHttpClient: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentDocumentService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for open file', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.openFile('fileId'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notice-of-intent-document'); + }); + + it('should show an error toast if opening a file fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.openFile('fileId'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a delete request for delete file', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.deleteExternalFile('fileId'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete.mock.calls[0][0]).toContain('notice-of-intent-document'); + }); + + it('should show an error toast if deleting a file fails', async () => { + mockHttpClient.delete.mockReturnValue(throwError(() => ({}))); + + await service.deleteExternalFile('fileId'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a patch request for update file', async () => { + mockHttpClient.patch.mockReturnValue(of({})); + + await service.update('fileId', []); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch.mock.calls[0][0]).toContain('notice-of-intent-document'); + }); + + it('should show an error toast if updating a file fails', async () => { + mockHttpClient.patch.mockReturnValue(throwError(() => ({}))); + + await service.update('fileId', []); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts new file mode 100644 index 0000000000..6b51562e5e --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts @@ -0,0 +1,91 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; +import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; +import { NoticeOfIntentDocumentDto, NoticeOfIntentDocumentUpdateDto } from './notice-of-intent-document.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentDocumentService { + private serviceUrl = `${environment.apiUrl}/notice-of-intent-document`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private documentService: DocumentService, + private overlayService: OverlaySpinnerService + ) {} + + async attachExternalFile( + fileNumber: string, + file: File, + documentType: DOCUMENT_TYPE | null, + source = DOCUMENT_SOURCE.APPLICANT + ) { + try { + const res = await this.documentService.uploadFile( + fileNumber, + file, + documentType, + source, + `${this.serviceUrl}/application/${fileNumber}/attachExternal` + ); + this.toastService.showSuccessToast('Document uploaded'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to attach document to Application, please try again'); + } + return undefined; + } + + async openFile(fileUuid: string) { + try { + return await firstValueFrom(this.httpClient.get<{ url: string }>(`${this.serviceUrl}/${fileUuid}/open`)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to open the document, please try again'); + } + return undefined; + } + + async deleteExternalFile(fileUuid: string) { + try { + this.overlayService.showSpinner(); + await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}/${fileUuid}`)); + this.toastService.showSuccessToast('Document deleted'); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete document, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + } + + async update(fileNumber: string | undefined, updateDtos: NoticeOfIntentDocumentUpdateDto[]) { + try { + await firstValueFrom(this.httpClient.patch(`${this.serviceUrl}/application/${fileNumber}`, updateDtos)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update documents, please try again'); + } + return undefined; + } + + async getByFileId(fileNumber: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/application/${fileNumber}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to fetch documents, please try again'); + } + return undefined; + } +} diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts new file mode 100644 index 0000000000..21b23da93c --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts @@ -0,0 +1,57 @@ +import { BaseCodeDto } from '../../shared/dto/base.dto'; +import { ApplicationDocumentDto } from '../application-document/application-document.dto'; +import { ApplicationParcelDto } from '../application-parcel/application-parcel.dto'; + +export enum OWNER_TYPE { + INDIVIDUAL = 'INDV', + ORGANIZATION = 'ORGZ', + AGENT = 'AGEN', + CROWN = 'CRWN', + GOVERNMENT = 'GOVR', +} + +export interface OwnerTypeDto extends BaseCodeDto { + code: OWNER_TYPE; +} + +export interface NoticeOfIntentOwnerDto { + uuid: string; + noticeOfIntentSubmissionUuid: string; + displayName: string; + firstName: string | null; + lastName: string | null; + organizationName: string | null; + phoneNumber: string | null; + email: string | null; + type: OwnerTypeDto; + corporateSummary?: ApplicationDocumentDto; +} + +export interface NoticeOfIntentOwnerDetailedDto extends NoticeOfIntentOwnerDto { + parcels: ApplicationParcelDto[]; +} + +export interface NoticeOfIntentOwnerUpdateDto { + firstName?: string | null; + lastName?: string | null; + organizationName?: string | null; + phoneNumber: string; + email: string; + typeCode: string; + corporateSummaryUuid?: string | null; +} + +export interface NoticeOfIntentOwnerCreateDto extends NoticeOfIntentOwnerUpdateDto { + noticeOfIntentSubmissionUuid: string; +} + +export interface SetPrimaryContactDto { + firstName?: string; + lastName?: string; + organization?: string; + phoneNumber?: string; + email?: string; + type?: OWNER_TYPE; + ownerUuid?: string; + noticeOfIntentSubmissionUuid: string; +} diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts new file mode 100644 index 0000000000..236396e06e --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts @@ -0,0 +1,189 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; + +import { NoticeOfIntentOwnerService } from './notice-of-intent-owner.service'; + +describe('NoticeOfIntentOwnerService', () => { + let service: NoticeOfIntentOwnerService; + let mockHttpClient: DeepMocked; + let mockToastService: DeepMocked; + let mockDocumentService: DeepMocked; + + let fileId = '123'; + + beforeEach(() => { + mockHttpClient = createMock(); + mockToastService = createMock(); + mockDocumentService = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentOwnerService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for loading owners', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.fetchBySubmissionId(fileId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if getting owners fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.fetchBySubmissionId(fileId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for create', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.create({ + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if creating owner fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.create({ + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a patch request for update', async () => { + mockHttpClient.patch.mockReturnValue(of({})); + + await service.update('', { + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if updating owner fails', async () => { + mockHttpClient.patch.mockReturnValue(throwError(() => ({}))); + + await service.update('', { + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a delete request for delete', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.delete(''); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if delete owner fails', async () => { + mockHttpClient.delete.mockReturnValue(throwError(() => ({}))); + + await service.delete(''); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for removeFromParcel', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.removeFromParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if removeFromParcel', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.removeFromParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for linkToParcel', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.linkToParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if linkToParcel', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.linkToParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for setPrimaryContact', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.setPrimaryContact({ noticeOfIntentSubmissionUuid: '' }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if setPrimaryContact', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.setPrimaryContact({ noticeOfIntentSubmissionUuid: '' }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts new file mode 100644 index 0000000000..92abf5b9e6 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts @@ -0,0 +1,144 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; +import { + NoticeOfIntentOwnerCreateDto, + NoticeOfIntentOwnerDto, + NoticeOfIntentOwnerUpdateDto, + SetPrimaryContactDto, +} from './notice-of-intent-owner.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentOwnerService { + private serviceUrl = `${environment.apiUrl}/notice-of-intent-owner`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private documentService: DocumentService + ) {} + + async fetchBySubmissionId(submissionUuid: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/submission/${submissionUuid}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Owners, please try again later'); + } + return undefined; + } + + async create(dto: NoticeOfIntentOwnerCreateDto) { + try { + const res = await firstValueFrom(this.httpClient.post(`${this.serviceUrl}`, dto)); + this.toastService.showSuccessToast('Owner created'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to create Owner, please try again later'); + return undefined; + } + } + + async update(uuid: string, updateDto: NoticeOfIntentOwnerUpdateDto) { + try { + const res = await firstValueFrom( + this.httpClient.patch(`${this.serviceUrl}/${uuid}`, updateDto) + ); + this.toastService.showSuccessToast('Owner saved'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Owner, please try again later'); + return undefined; + } + } + + async setPrimaryContact(updateDto: SetPrimaryContactDto) { + try { + const res = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/setPrimaryContact`, updateDto) + ); + this.toastService.showSuccessToast('Application saved'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Application, please try again later'); + return undefined; + } + } + + async delete(uuid: string) { + try { + const result = await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}/${uuid}`)); + this.toastService.showSuccessToast('Owner deleted'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete Owner, please try again'); + } + return undefined; + } + + async removeFromParcel(ownerUuid: string, parcelUuid: string) { + try { + const result = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/${ownerUuid}/unlink/${parcelUuid}`, {}) + ); + this.toastService.showSuccessToast('Owner removed from parcel'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to remove Owner, please try again'); + } + return undefined; + } + + async linkToParcel(ownerUuid: any, parcelUuid: string) { + try { + const result = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/${ownerUuid}/link/${parcelUuid}`, {}) + ); + this.toastService.showSuccessToast('Owner linked to parcel'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to link Owner, please try again'); + } + return undefined; + } + + sortOwners(a: NoticeOfIntentOwnerDto, b: NoticeOfIntentOwnerDto) { + if (a.displayName < b.displayName) { + return -1; + } + if (a.displayName > b.displayName) { + return 1; + } + return 0; + } + + async uploadCorporateSummary(applicationFileId: string, file: File) { + try { + return await this.documentService.uploadFile<{ uuid: string }>( + applicationFileId, + file, + DOCUMENT_TYPE.CORPORATE_SUMMARY, + DOCUMENT_SOURCE.APPLICANT, + `${this.serviceUrl}/attachCorporateSummary` + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to attach document to Owner, please try again'); + } + return undefined; + } +} diff --git a/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts new file mode 100644 index 0000000000..7b5e10b9cd --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts @@ -0,0 +1,29 @@ +import { BaseCodeDto } from '../../shared/dto/base.dto'; +import { ApplicationDocumentDto } from '../application-document/application-document.dto'; +import { NoticeOfIntentOwnerDto } from '../notice-of-intent-owner/notice-of-intent-owner.dto'; + +export interface NoticeOfIntentParcelUpdateDto { + uuid: string; + pid?: string | null; + pin?: string | null; + civicAddress?: string | null; + legalDescription?: string | null; + mapAreaHectares?: string | null; + purchasedDate?: number | null; + isFarm?: boolean | null; + ownershipTypeCode?: string | null; + crownLandOwnerType?: string | null; + isConfirmedByApplicant: boolean; + ownerUuids: string[] | null; +} + +export interface NoticeOfIntentParcelDto extends Omit { + ownershipType?: BaseCodeDto; + owners: NoticeOfIntentOwnerDto[]; + certificateOfTitle?: ApplicationDocumentDto; +} + +export enum PARCEL_OWNERSHIP_TYPE { + FEE_SIMPLE = 'SMPL', + CROWN = 'CRWN', +} diff --git a/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts new file mode 100644 index 0000000000..735991e006 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts @@ -0,0 +1,102 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ToastService } from '../toast/toast.service'; + +import { of, throwError } from 'rxjs'; +import { DocumentService } from '../document/document.service'; +import { NoticeOfIntentParcelUpdateDto } from './notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel.service'; + +describe('NoticeOfIntentParcelService', () => { + let service: NoticeOfIntentParcelService; + let mockHttpClient: DeepMocked; + let mockDocumentService: DeepMocked; + let mockToastService: DeepMocked; + + const mockUuid = 'fake_uuid'; + + beforeEach(() => { + mockHttpClient = createMock(); + mockToastService = createMock(); + + TestBed.configureTestingModule({ + imports: [], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentParcelService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for loading parcels', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.fetchBySubmissionUuid(mockUuid); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notice-of-intent-parcel'); + }); + + it('should show an error toast if getting parcel fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.fetchBySubmissionUuid(mockUuid); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for create', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.create(mockUuid); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-parcel'); + }); + + it('should show an error toast if creating a parcel fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.create(mockUuid); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a put request for update', async () => { + mockHttpClient.put.mockReturnValue(of({})); + let mockUuid = 'fake'; + + await service.update([{ uuid: mockUuid }] as NoticeOfIntentParcelUpdateDto[]); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockToastService.showSuccessToast).toHaveBeenCalledTimes(1); + expect(mockHttpClient.put.mock.calls[0][0]).toContain('notice-of-intent-parcel'); + }); + + it('should show an error toast if updating a parcel fails', async () => { + mockHttpClient.put.mockReturnValue(throwError(() => ({}))); + + await service.update([{}] as NoticeOfIntentParcelUpdateDto[]); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts new file mode 100644 index 0000000000..b468fd8cf7 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts @@ -0,0 +1,107 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; +import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { DocumentService } from '../document/document.service'; +import { NoticeOfIntentDocumentDto } from '../notice-of-intent-document/notice-of-intent-document.dto'; +import { ToastService } from '../toast/toast.service'; +import { NoticeOfIntentParcelDto, NoticeOfIntentParcelUpdateDto } from './notice-of-intent-parcel.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentParcelService { + private serviceUrl = `${environment.apiUrl}/notice-of-intent-parcel`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private documentService: DocumentService, + private overlayService: OverlaySpinnerService + ) {} + + async fetchBySubmissionUuid(submissionUuid: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/submission/${submissionUuid}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Parcel, please try again later'); + } + return undefined; + } + + async create(noticeOfIntentSubmissionUuid: string, ownerUuid?: string) { + try { + return await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}`, { + noticeOfIntentSubmissionUuid, + ownerUuid, + }) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to create Parcel, please try again later'); + return undefined; + } + } + + async update(updateDtos: NoticeOfIntentParcelUpdateDto[]) { + try { + this.overlayService.showSpinner(); + const formattedDtos = updateDtos.map((e) => ({ + ...e, + mapAreaHectares: e.mapAreaHectares ? parseFloat(e.mapAreaHectares) : e.mapAreaHectares, + })); + + const result = await firstValueFrom( + this.httpClient.put(`${this.serviceUrl}`, formattedDtos) + ); + + this.toastService.showSuccessToast('Parcel saved'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Parcel, please try again later'); + } finally { + this.overlayService.hideSpinner(); + } + return undefined; + } + + async attachCertificateOfTitle(fileId: string, parcelUuid: string, file: File) { + try { + const document = await this.documentService.uploadFile( + fileId, + file, + DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + DOCUMENT_SOURCE.APPLICANT, + `${this.serviceUrl}/${parcelUuid}/attachCertificateOfTitle` + ); + this.toastService.showSuccessToast('Document uploaded'); + return document; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to attach document to Parcel, please try again'); + } + return undefined; + } + + async deleteMany(parcelUuids: string[]) { + try { + this.overlayService.showSpinner(); + const result = await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}`, { body: parcelUuids })); + this.toastService.showSuccessToast('Parcel deleted'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete Parcel, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + return undefined; + } +} diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts index cc747f826b..242403d17c 100644 --- a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -1,4 +1,5 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; +import { NoticeOfIntentOwnerDto } from '../notice-of-intent-owner/notice-of-intent-owner.dto'; export enum NOI_SUBMISSION_STATUS { IN_PROGRESS = 'PROG', @@ -32,6 +33,7 @@ export interface NoticeOfIntentSubmissionDto { type: string; status: NoticeOfIntentSubmissionStatusDto; submissionStatuses: NoticeOfIntentSubmissionToSubmissionStatusDto[]; + owners: NoticeOfIntentOwnerDto[]; canEdit: boolean; canView: boolean; } diff --git a/portal-frontend/src/app/shared/dto/document.dto.ts b/portal-frontend/src/app/shared/dto/document.dto.ts new file mode 100644 index 0000000000..e2dfebeef6 --- /dev/null +++ b/portal-frontend/src/app/shared/dto/document.dto.ts @@ -0,0 +1,41 @@ +import { BaseCodeDto } from './base.dto'; + +export enum DOCUMENT_TYPE { + //ALCS + DECISION_DOCUMENT = 'DPAC', + OTHER = 'OTHR', + + //Government Review + RESOLUTION_DOCUMENT = 'RESO', + STAFF_REPORT = 'STFF', + + //Applicant Uploaded + CORPORATE_SUMMARY = 'CORS', + PROFESSIONAL_REPORT = 'PROR', + PHOTOGRAPH = 'PHTO', + AUTHORIZATION_LETTER = 'AAGR', + CERTIFICATE_OF_TITLE = 'CERT', + + //App Documents + SERVING_NOTICE = 'POSN', + PROPOSAL_MAP = 'PRSK', + HOMESITE_SEVERANCE = 'HOME', + CROSS_SECTIONS = 'SPCS', + RECLAMATION_PLAN = 'RECP', + NOTICE_OF_WORK = 'NOWE', + PROOF_OF_SIGNAGE = 'POSA', + REPORT_OF_PUBLIC_HEARING = 'ROPH', + PROOF_OF_ADVERTISING = 'POAA', +} + +export enum DOCUMENT_SOURCE { + APPLICANT = 'Applicant', + ALC = 'ALC', + LFNG = 'L/FNG', + AFFECTED_PARTY = 'Affected Party', + PUBLIC = 'Public', +} + +export interface DocumentTypeDto extends BaseCodeDto { + code: DOCUMENT_TYPE; +} diff --git a/portal-frontend/src/app/shared/dto/owner.dto.ts b/portal-frontend/src/app/shared/dto/owner.dto.ts new file mode 100644 index 0000000000..9e1d82879a --- /dev/null +++ b/portal-frontend/src/app/shared/dto/owner.dto.ts @@ -0,0 +1,13 @@ +import { BaseCodeDto } from './base.dto'; + +export enum OWNER_TYPE { + INDIVIDUAL = 'INDV', + ORGANIZATION = 'ORGZ', + AGENT = 'AGEN', + CROWN = 'CRWN', + GOVERNMENT = 'GOVR', +} + +export interface OwnerTypeDto extends BaseCodeDto { + code: OWNER_TYPE; +} diff --git a/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts b/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts index c91b4a0d59..f93e8d68c2 100644 --- a/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts +++ b/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts @@ -42,7 +42,7 @@ export class ApplicationDocumentService { @InjectRepository(ApplicationDocument) private applicationDocumentRepository: Repository, @InjectRepository(DocumentCode) - private applicationDocumentCodeRepository: Repository, + private documentCodeRepository: Repository, ) {} async attachDocument({ @@ -253,7 +253,7 @@ export class ApplicationDocumentService { } async fetchTypes() { - return await this.applicationDocumentCodeRepository.find(); + return await this.documentCodeRepository.find(); } async update({ diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.controller.spec.ts index 84513e6ff9..3891462cba 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.controller.spec.ts @@ -9,9 +9,7 @@ import { NoticeOfIntentProfile } from '../../../common/automapper/notice-of-inte import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; import { DOCUMENT_SOURCE } from '../../../document/document.dto'; import { Document } from '../../../document/document.entity'; -import { ApplicationOwner } from '../../../portal/application-submission/application-owner/application-owner.entity'; import { ApplicationOwnerService } from '../../../portal/application-submission/application-owner/application-owner.service'; -import { ApplicationParcel } from '../../../portal/application-submission/application-parcel/application-parcel.entity'; import { ApplicationParcelService } from '../../../portal/application-submission/application-parcel/application-parcel.service'; import { User } from '../../../user/user.entity'; import { CodeService } from '../../code/code.service'; diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts index bd0039567e..91509763b4 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts @@ -1,5 +1,9 @@ import { MultipartFile } from '@fastify/multipart'; -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ArrayOverlap, @@ -16,6 +20,7 @@ import { DOCUMENT_SYSTEM, } from '../../../document/document.dto'; import { DocumentService } from '../../../document/document.service'; +import { PortalNoticeOfIntentDocumentUpdateDto } from '../../../portal/notice-of-intent-document/notice-of-intent-document.dto'; import { User } from '../../../user/user.entity'; import { NoticeOfIntentService } from '../notice-of-intent.service'; import { @@ -197,37 +202,36 @@ export class NoticeOfIntentDocumentService { ); return this.get(savedDocument.uuid); } - // TODO: Re-enable as part of creating Step 7 - // async updateDescriptionAndType( - // updates: PortalNoticeOfIntentDocumentUpdateDto[], - // noticeOfIntentUuid: string, - // ) { - // const results: NoticeOfIntentDocument[] = []; - // for (const update of updates) { - // const file = await this.noticeOfIntentDocumentRepository.findOne({ - // where: { - // uuid: update.uuid, - // noticeOfIntentUuid, - // }, - // relations: { - // document: true, - // }, - // }); - // if (!file) { - // throw new BadRequestException( - // 'Failed to find file linked to provided noticeOfIntent', - // ); - // } - // - // file.typeCode = update.type; - // file.description = update.description; - // const updatedFile = await this.noticeOfIntentDocumentRepository.save( - // file, - // ); - // results.push(updatedFile); - // } - // return results; - // } + async updateDescriptionAndType( + updates: PortalNoticeOfIntentDocumentUpdateDto[], + noticeOfIntentUuid: string, + ) { + const results: NoticeOfIntentDocument[] = []; + for (const update of updates) { + const file = await this.noticeOfIntentDocumentRepository.findOne({ + where: { + uuid: update.uuid, + noticeOfIntentUuid, + }, + relations: { + document: true, + }, + }); + if (!file) { + throw new BadRequestException( + 'Failed to find file linked to provided noticeOfIntent', + ); + } + + file.typeCode = update.type; + file.description = update.description; + const updatedFile = await this.noticeOfIntentDocumentRepository.save( + file, + ); + results.push(updatedFile); + } + return results; + } async deleteByType(documentType: DOCUMENT_TYPE, noticeOfIntentUuid: string) { const documents = await this.noticeOfIntentDocumentRepository.find({ diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts index 876e7e05d3..a33b7ba440 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts @@ -341,10 +341,10 @@ export class NoticeOfIntentService { fileNumber, }, select: { - fileNumber: true, + uuid: true, }, }); - return noticeOfIntent.fileNumber; + return noticeOfIntent.uuid; } async submit(createDto: CreateNoticeOfIntentServiceDto) { diff --git a/services/apps/alcs/src/common/automapper/application-owner.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-owner.automapper.profile.ts index f980f99fb6..5b357761a8 100644 --- a/services/apps/alcs/src/common/automapper/application-owner.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-owner.automapper.profile.ts @@ -3,11 +3,10 @@ import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { ApplicationDocumentDto } from '../../alcs/application/application-document/application-document.dto'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; -import { ApplicationOwnerType } from '../../portal/application-submission/application-owner/application-owner-type/application-owner-type.entity'; +import { OwnerType, OwnerTypeDto } from '../owner-type/owner-type.entity'; import { ApplicationOwnerDetailedDto, ApplicationOwnerDto, - ApplicationOwnerTypeDto, } from '../../portal/application-submission/application-owner/application-owner.dto'; import { ApplicationOwner } from '../../portal/application-submission/application-owner/application-owner.entity'; import { ApplicationParcelDto } from '../../portal/application-submission/application-parcel/application-parcel.dto'; @@ -77,7 +76,7 @@ export class ApplicationOwnerProfile extends AutomapperProfile { ), ); - createMap(mapper, ApplicationOwnerType, ApplicationOwnerTypeDto); + createMap(mapper, OwnerType, OwnerTypeDto); }; } } diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-owner.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-owner.automapper.profile.ts new file mode 100644 index 0000000000..ad4734b3fe --- /dev/null +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-owner.automapper.profile.ts @@ -0,0 +1,82 @@ +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { NoticeOfIntentDocumentDto } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { + NoticeOfIntentOwnerDetailedDto, + NoticeOfIntentOwnerDto, +} from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentParcelDto } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { OwnerType, OwnerTypeDto } from '../owner-type/owner-type.entity'; + +@Injectable() +export class NoticeOfIntentOwnerProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap( + mapper, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + forMember( + (pd) => pd.displayName, + mapFrom((p) => `${p.firstName} ${p.lastName}`), + ), + forMember( + (pd) => pd.corporateSummary, + mapFrom((p) => + p.corporateSummary + ? this.mapper.map( + p.corporateSummary, + NoticeOfIntentDocument, + NoticeOfIntentDocumentDto, + ) + : undefined, + ), + ), + ); + + createMap( + mapper, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDetailedDto, + forMember( + (pd) => pd.displayName, + mapFrom((p) => `${p.firstName} ${p.lastName}`), + ), + forMember( + (ad) => ad.parcels, + mapFrom((a) => { + if (a.parcels) { + return this.mapper.mapArray( + a.parcels, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + }), + ), + forMember( + (pd) => pd.corporateSummary, + mapFrom((p) => + p.corporateSummary + ? this.mapper.map( + p.corporateSummary, + NoticeOfIntentDocument, + NoticeOfIntentDocumentDto, + ) + : undefined, + ), + ), + ); + + createMap(mapper, OwnerType, OwnerTypeDto); + }; + } +} diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts new file mode 100644 index 0000000000..eb57ac85aa --- /dev/null +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts @@ -0,0 +1,85 @@ +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { NoticeOfIntentDocumentDto } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { NoticeOfIntentOwnerDto } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentParcelOwnershipType } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity'; +import { + NoticeOfIntentParcelDto, + NoticeOfIntentParcelOwnershipTypeDto, +} from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; + +@Injectable() +export class NoticeOfIntentParcelProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap( + mapper, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + forMember( + (pd) => pd.ownershipTypeCode, + mapFrom((p) => p.ownershipTypeCode), + ), + forMember( + (pd) => pd.purchasedDate, + mapFrom((p) => p.purchasedDate?.getTime()), + ), + forMember( + (p) => p.certificateOfTitle, + mapFrom((pd) => { + if (pd.certificateOfTitle) { + return this.mapper.map( + pd.certificateOfTitle, + NoticeOfIntentDocument, + NoticeOfIntentDocumentDto, + ); + } + return; + }), + ), + forMember( + (p) => p.owners, + mapFrom((pd) => { + if (pd.owners) { + return this.mapper.mapArray( + pd.owners, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + } else { + return []; + } + }), + ), + forMember( + (p) => p.ownershipType, + mapFrom((pd) => { + if (pd.ownershipType) { + return this.mapper.map( + pd.ownershipType, + NoticeOfIntentParcelOwnershipType, + NoticeOfIntentParcelOwnershipTypeDto, + ); + } else { + return undefined; + } + }), + ), + ); + + createMap( + mapper, + NoticeOfIntentParcelOwnershipType, + NoticeOfIntentParcelOwnershipTypeDto, + ); + }; + } +} diff --git a/services/apps/alcs/src/common/owner-type/owner-type.entity.ts b/services/apps/alcs/src/common/owner-type/owner-type.entity.ts new file mode 100644 index 0000000000..558d3e31ca --- /dev/null +++ b/services/apps/alcs/src/common/owner-type/owner-type.entity.ts @@ -0,0 +1,23 @@ +import { Entity } from 'typeorm'; +import { BaseCodeDto } from '../dtos/base.dto'; +import { BaseCodeEntity } from '../entities/base.code.entity'; + +export enum OWNER_TYPE { + INDIVIDUAL = 'INDV', + ORGANIZATION = 'ORGZ', + AGENT = 'AGEN', + CROWN = 'CRWN', + GOVERNMENT = 'GOVR', +} + +export class OwnerTypeDto extends BaseCodeDto {} + +@Entity() +export class OwnerType extends BaseCodeEntity { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } +} diff --git a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts index 53cdc35914..fc9d96e259 100644 --- a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts +++ b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationSubmissionStatusModule } from '../../alcs/application/application-submission-status/application-submission-status.module'; import { ApplicationSubmissionStatusType } from '../../alcs/application/application-submission-status/submission-status-type.entity'; -import { ApplicationOwnerType } from '../application-submission/application-owner/application-owner-type/application-owner-type.entity'; +import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; import { ApplicationParcelOwnershipType } from '../application-submission/application-parcel/application-parcel-ownership-type/application-parcel-ownership-type.entity'; import { ApplicationParcel } from '../application-submission/application-parcel/application-parcel.entity'; @@ -20,7 +20,7 @@ import { ApplicationSubmissionDraftService } from './application-submission-draf ApplicationParcel, ApplicationParcelOwnershipType, ApplicationOwner, - ApplicationOwnerType, + OwnerType, ]), ApplicationSubmissionModule, PdfGenerationModule, diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts index cb59406379..370f48ebbd 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts @@ -21,7 +21,7 @@ import { DOCUMENT_SOURCE } from '../../document/document.dto'; import { Document } from '../../document/document.entity'; import { EmailService } from '../../providers/email/email.service'; import { User } from '../../user/user.entity'; -import { ApplicationOwnerType } from '../application-submission/application-owner/application-owner-type/application-owner-type.entity'; +import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; import { ApplicationSubmissionValidatorService, @@ -253,7 +253,7 @@ describe('ApplicationSubmissionReviewController', () => { new ApplicationOwner({ uuid: 'uuid', email: 'fake-email', - type: new ApplicationOwnerType(), + type: new OwnerType(), }), ], primaryContactOwnerUuid: 'uuid', diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index c0d70aa686..ebec8444d1 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -14,17 +14,17 @@ import { UseGuards, } from '@nestjs/common'; import { generateStatusHtml } from '../../../../../templates/emails/submission-status.template'; -import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; -import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; -import { ApplicationService } from '../../alcs/application/application.service'; import { ApplicationSubmissionStatusService } from '../../alcs/application/application-submission-status/application-submission-status.service'; import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; +import { ApplicationService } from '../../alcs/application/application.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; +import { OWNER_TYPE } from '../../common/owner-type/owner-type.entity'; import { DOCUMENT_SOURCE } from '../../document/document.dto'; import { EmailService } from '../../providers/email/email.service'; import { User } from '../../user/user.entity'; -import { APPLICATION_OWNER } from '../application-submission/application-owner/application-owner.dto'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; import { ApplicationSubmissionValidatorService } from '../application-submission/application-submission-validator.service'; import { ApplicationSubmission } from '../application-submission/application-submission.entity'; @@ -215,7 +215,7 @@ export class ApplicationSubmissionReviewController { if ( creatingGovernment?.uuid === applicationSubmission.localGovernmentUuid && primaryContact && - primaryContact.type.code === APPLICATION_OWNER.GOVERNMENT + primaryContact.type.code === OWNER_TYPE.GOVERNMENT ) { //Copy contact details over to government form when applying to self await this.applicationSubmissionReviewService.update( diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner-type/application-owner-type.entity.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner-type/application-owner-type.entity.ts deleted file mode 100644 index ecbfbad3a2..0000000000 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner-type/application-owner-type.entity.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Entity } from 'typeorm'; -import { BaseCodeEntity } from '../../../../common/entities/base.code.entity'; - -@Entity() -export class ApplicationOwnerType extends BaseCodeEntity { - constructor(data?: Partial) { - super(); - if (data) { - Object.assign(this, data); - } - } -} diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts index 8aa0a7ddcb..d4decd8739 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts @@ -7,12 +7,14 @@ import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; import { ApplicationDocumentService } from '../../../alcs/application/application-document/application-document.service'; import { ApplicationOwnerProfile } from '../../../common/automapper/application-owner.automapper.profile'; +import { + OWNER_TYPE, + OwnerType, +} from '../../../common/owner-type/owner-type.entity'; import { DocumentService } from '../../../document/document.service'; import { ApplicationSubmission } from '../application-submission.entity'; import { ApplicationSubmissionService } from '../application-submission.service'; -import { ApplicationOwnerType } from './application-owner-type/application-owner-type.entity'; import { ApplicationOwnerController } from './application-owner.controller'; -import { APPLICATION_OWNER } from './application-owner.dto'; import { ApplicationOwner } from './application-owner.entity'; import { ApplicationOwnerService } from './application-owner.service'; @@ -282,8 +284,8 @@ describe('ApplicationOwnerController', () => { it('should set the owner and delete agents when using a non-agent owner', async () => { mockAppOwnerService.getOwner.mockResolvedValue( new ApplicationOwner({ - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.INDIVIDUAL, + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, }), }), ); @@ -312,8 +314,8 @@ describe('ApplicationOwnerController', () => { it('should update the agent owner when calling set primary contact', async () => { mockAppOwnerService.getOwner.mockResolvedValue( new ApplicationOwner({ - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.AGENT, + type: new OwnerType({ + code: OWNER_TYPE.AGENT, }), }), ); diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts index 7a0cbe42fe..508340d81a 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts @@ -12,10 +12,11 @@ import { Req, UseGuards, } from '@nestjs/common'; -import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; import { VISIBILITY_FLAG } from '../../../alcs/application/application-document/application-document.entity'; import { ApplicationDocumentService } from '../../../alcs/application/application-document/application-document.service'; import { PortalAuthGuard } from '../../../common/authorization/portal-auth-guard.service'; +import { OWNER_TYPE } from '../../../common/owner-type/owner-type.entity'; +import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, @@ -23,7 +24,6 @@ import { import { DocumentService } from '../../../document/document.service'; import { ApplicationSubmissionService } from '../application-submission.service'; import { - APPLICATION_OWNER, ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, @@ -133,7 +133,7 @@ export class ApplicationOwnerController { dto: ApplicationOwnerUpdateDto | ApplicationOwnerCreateDto, ) { if ( - dto.typeCode === APPLICATION_OWNER.INDIVIDUAL && + dto.typeCode === OWNER_TYPE.INDIVIDUAL && (!dto.firstName || !dto.lastName) ) { throw new BadRequestException( @@ -141,10 +141,7 @@ export class ApplicationOwnerController { ); } - if ( - dto.typeCode === APPLICATION_OWNER.ORGANIZATION && - !dto.organizationName - ) { + if (dto.typeCode === OWNER_TYPE.ORGANIZATION && !dto.organizationName) { throw new BadRequestException( 'Organizations must have an organizationName', ); @@ -184,8 +181,8 @@ export class ApplicationOwnerController { ); if ( - primaryContactOwner.type.code === APPLICATION_OWNER.AGENT || - primaryContactOwner.type.code === APPLICATION_OWNER.GOVERNMENT + primaryContactOwner.type.code === OWNER_TYPE.AGENT || + primaryContactOwner.type.code === OWNER_TYPE.GOVERNMENT ) { //Update Fields for non parcel owners await this.ownerService.update(primaryContactOwner.uuid, { diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts index 10ec1bdac7..43b3bbf60a 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts @@ -8,19 +8,13 @@ import { } from 'class-validator'; import { ApplicationDocumentDto } from '../../../alcs/application/application-document/application-document.dto'; import { BaseCodeDto } from '../../../common/dtos/base.dto'; +import { + OWNER_TYPE, + OwnerTypeDto, +} from '../../../common/owner-type/owner-type.entity'; import { emailRegex } from '../../../utils/email.helper'; import { ApplicationParcelDto } from '../application-parcel/application-parcel.dto'; -export enum APPLICATION_OWNER { - INDIVIDUAL = 'INDV', - ORGANIZATION = 'ORGZ', - AGENT = 'AGEN', - CROWN = 'CRWN', - GOVERNMENT = 'GOVR', -} - -export class ApplicationOwnerTypeDto extends BaseCodeDto {} - export class ApplicationOwnerDto { @AutoMap() uuid: string; @@ -49,7 +43,7 @@ export class ApplicationOwnerDto { email?: string | null; @AutoMap() - type: ApplicationOwnerTypeDto; + type: OwnerTypeDto; @AutoMap(() => ApplicationDocumentDto) corporateSummary?: ApplicationDocumentDto; @@ -117,7 +111,7 @@ export class SetPrimaryContactDto { @IsString() @IsOptional() - type?: APPLICATION_OWNER; + type?: OWNER_TYPE; @IsUUID() @IsOptional() diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.entity.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.entity.ts index 8f830e32ef..a24fec78da 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.entity.ts @@ -5,7 +5,7 @@ import { ApplicationDocument } from '../../../alcs/application/application-docum import { Base } from '../../../common/entities/base.entity'; import { ApplicationParcel } from '../application-parcel/application-parcel.entity'; import { ApplicationSubmission } from '../application-submission.entity'; -import { ApplicationOwnerType } from './application-owner-type/application-owner-type.entity'; +import { OwnerType } from '../../../common/owner-type/owner-type.entity'; @Entity() export class ApplicationOwner extends Base { @@ -62,8 +62,8 @@ export class ApplicationOwner extends Base { corporateSummaryUuid: string | null; @AutoMap() - @ManyToOne(() => ApplicationOwnerType, { nullable: false }) - type: ApplicationOwnerType; + @ManyToOne(() => OwnerType, { nullable: false }) + type: OwnerType; @ManyToOne(() => ApplicationSubmission, { nullable: false }) applicationSubmission: ApplicationSubmission; diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts index b78ca5feb0..4fc9af391b 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts @@ -9,7 +9,7 @@ import { ApplicationParcel } from '../application-parcel/application-parcel.enti import { ApplicationParcelService } from '../application-parcel/application-parcel.service'; import { ApplicationSubmission } from '../application-submission.entity'; import { ApplicationSubmissionService } from '../application-submission.service'; -import { ApplicationOwnerType } from './application-owner-type/application-owner-type.entity'; +import { OwnerType } from '../../../common/owner-type/owner-type.entity'; import { ApplicationOwner } from './application-owner.entity'; import { ApplicationOwnerService } from './application-owner.service'; @@ -17,7 +17,7 @@ describe('ApplicationOwnerService', () => { let service: ApplicationOwnerService; let mockParcelService: DeepMocked; let mockRepo: DeepMocked>; - let mockTypeRepo: DeepMocked>; + let mockTypeRepo: DeepMocked>; let mockAppDocumentService: DeepMocked; let mockApplicationservice: DeepMocked; @@ -40,7 +40,7 @@ describe('ApplicationOwnerService', () => { useValue: mockRepo, }, { - provide: getRepositoryToken(ApplicationOwnerType), + provide: getRepositoryToken(OwnerType), useValue: mockTypeRepo, }, { @@ -80,7 +80,7 @@ describe('ApplicationOwnerService', () => { it('should load the type and then call save for create', async () => { mockRepo.save.mockResolvedValue(new ApplicationOwner()); - mockTypeRepo.findOneOrFail.mockResolvedValue(new ApplicationOwnerType()); + mockTypeRepo.findOneOrFail.mockResolvedValue(new OwnerType()); await service.create( { diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts index ddc745c66a..81e5dde54b 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts @@ -2,13 +2,15 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Any, Repository } from 'typeorm'; import { ApplicationDocumentService } from '../../../alcs/application/application-document/application-document.service'; +import { + OWNER_TYPE, + OwnerType, +} from '../../../common/owner-type/owner-type.entity'; import { PARCEL_TYPE } from '../application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../application-parcel/application-parcel.service'; import { ApplicationSubmission } from '../application-submission.entity'; import { ApplicationSubmissionService } from '../application-submission.service'; -import { ApplicationOwnerType } from './application-owner-type/application-owner-type.entity'; import { - APPLICATION_OWNER, ApplicationOwnerCreateDto, ApplicationOwnerUpdateDto, } from './application-owner.dto'; @@ -19,8 +21,8 @@ export class ApplicationOwnerService { constructor( @InjectRepository(ApplicationOwner) private repository: Repository, - @InjectRepository(ApplicationOwnerType) - private typeRepository: Repository, + @InjectRepository(OwnerType) + private typeRepository: Repository, @Inject(forwardRef(() => ApplicationParcelService)) private applicationParcelService: ApplicationParcelService, @Inject(forwardRef(() => ApplicationSubmissionService)) @@ -222,7 +224,7 @@ export class ApplicationOwnerService { uuid: submissionUuid, }, type: { - code: APPLICATION_OWNER.AGENT, + code: OWNER_TYPE.AGENT, }, }, { @@ -230,7 +232,7 @@ export class ApplicationOwnerService { uuid: submissionUuid, }, type: { - code: APPLICATION_OWNER.GOVERNMENT, + code: OWNER_TYPE.GOVERNMENT, }, }, ], diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts index 1e6e28cb0b..4c11f99ea3 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts @@ -11,8 +11,10 @@ import { ApplicationDocument } from '../../alcs/application/application-document import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; import { DOCUMENT_SOURCE } from '../../document/document.dto'; import { Document } from '../../document/document.entity'; -import { ApplicationOwnerType } from './application-owner/application-owner-type/application-owner-type.entity'; -import { APPLICATION_OWNER } from './application-owner/application-owner.dto'; +import { + OWNER_TYPE, + OwnerType, +} from '../../common/owner-type/owner-type.entity'; import { ApplicationOwner } from './application-owner/application-owner.entity'; import { PARCEL_TYPE } from './application-parcel/application-parcel.dto'; import { ApplicationParcel } from './application-parcel/application-parcel.entity'; @@ -174,8 +176,8 @@ describe('ApplicationSubmissionValidatorService', () => { const applicationSubmission = new ApplicationSubmission({ owners: [ new ApplicationOwner({ - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.CROWN, + type: new OwnerType({ + code: OWNER_TYPE.CROWN, }), }), ], @@ -255,8 +257,8 @@ describe('ApplicationSubmissionValidatorService', () => { it('should return errors for an invalid third party agent', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.AGENT, + type: new OwnerType({ + code: OWNER_TYPE.AGENT, }), firstName: 'Bruce', lastName: 'Wayne', @@ -279,8 +281,8 @@ describe('ApplicationSubmissionValidatorService', () => { it('should require an authorization letter for more than one owner', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.AGENT, + type: new OwnerType({ + code: OWNER_TYPE.AGENT, }), firstName: 'Bruce', lastName: 'Wayne', @@ -303,8 +305,8 @@ describe('ApplicationSubmissionValidatorService', () => { it('should not require an authorization letter for a single owner', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.INDIVIDUAL, + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, }), firstName: 'Bruce', lastName: 'Wayne', @@ -327,8 +329,8 @@ describe('ApplicationSubmissionValidatorService', () => { it('should not require an authorization letter when contact is goverment', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.INDIVIDUAL, + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, }), firstName: 'Bruce', lastName: 'Wayne', @@ -336,8 +338,8 @@ describe('ApplicationSubmissionValidatorService', () => { const governmentOwner = new ApplicationOwner({ uuid: 'government-owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.GOVERNMENT, + type: new OwnerType({ + code: OWNER_TYPE.GOVERNMENT, }), firstName: 'Govern', lastName: 'Ment', @@ -361,8 +363,8 @@ describe('ApplicationSubmissionValidatorService', () => { it('should not have an authorization letter error when one is provided', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.INDIVIDUAL, + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, }), firstName: 'Bruce', lastName: 'Wayne', diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts index aed07bd264..7985577d18 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts @@ -1,10 +1,10 @@ import { ServiceValidationException } from '@app/common/exceptions/base.exception'; import { Injectable, Logger } from '@nestjs/common'; -import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; -import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; -import { APPLICATION_OWNER } from './application-owner/application-owner.dto'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { OWNER_TYPE } from '../../common/owner-type/owner-type.entity'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationOwner } from './application-owner/application-owner.entity'; import { PARCEL_TYPE } from './application-parcel/application-parcel.dto'; import { ApplicationParcel } from './application-parcel/application-parcel.entity'; @@ -260,11 +260,10 @@ export class ApplicationSubmissionValidatorService { const onlyHasIndividualOwner = applicationSubmission.owners.length === 1 && - applicationSubmission.owners[0].type.code === - APPLICATION_OWNER.INDIVIDUAL; + applicationSubmission.owners[0].type.code === OWNER_TYPE.INDIVIDUAL; const isGovernmentContact = - primaryOwner.type.code === APPLICATION_OWNER.GOVERNMENT; + primaryOwner.type.code === OWNER_TYPE.GOVERNMENT; if (!onlyHasIndividualOwner && !isGovernmentContact) { const authorizationLetters = documents.filter( @@ -280,10 +279,7 @@ export class ApplicationSubmissionValidatorService { } } - if ( - primaryOwner.type.code === APPLICATION_OWNER.AGENT || - isGovernmentContact - ) { + if (primaryOwner.type.code === OWNER_TYPE.AGENT || isGovernmentContact) { if ( !primaryOwner.firstName || !primaryOwner.lastName || diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.module.ts b/services/apps/alcs/src/portal/application-submission/application-submission.module.ts index 2ffb29d04c..8960faeeae 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.module.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.module.ts @@ -11,7 +11,7 @@ import { ApplicationSubmissionProfile } from '../../common/automapper/applicatio import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; import { PdfGenerationModule } from '../pdf-generation/pdf-generation.module'; -import { ApplicationOwnerType } from './application-owner/application-owner-type/application-owner-type.entity'; +import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { ApplicationOwnerController } from './application-owner/application-owner.controller'; import { ApplicationOwner } from './application-owner/application-owner.entity'; import { ApplicationOwnerService } from './application-owner/application-owner.service'; @@ -33,7 +33,7 @@ import { NaruSubtype } from './naru-subtype/naru-subtype.entity'; ApplicationParcel, ApplicationParcelOwnershipType, ApplicationOwner, - ApplicationOwnerType, + OwnerType, NaruSubtype, ApplicationSubmissionToSubmissionStatus, ]), diff --git a/services/apps/alcs/src/portal/code/code.controller.ts b/services/apps/alcs/src/portal/code/code.controller.ts index 41fa24abe5..8de46fd499 100644 --- a/services/apps/alcs/src/portal/code/code.controller.ts +++ b/services/apps/alcs/src/portal/code/code.controller.ts @@ -39,14 +39,13 @@ export class CodeController { const localGovernments = await this.localGovernmentService.listActive(); const applicationTypes = await this.applicationService.fetchApplicationTypes(); - const applicationDocumentTypes = - await this.applicationDocumentService.fetchTypes(); + const documentTypes = await this.applicationDocumentService.fetchTypes(); const submissionTypes = await this.cardService.getPortalCardTypes(); const noticeOfIntentTypes = await this.noticeOfIntentService.listTypes(); const naruSubtypes = await this.applicationSubmissionService.listNaruSubtypes(); - const mappedDocTypes = applicationDocumentTypes.map((docType) => { + const mappedDocTypes = documentTypes.map((docType) => { if (docType.portalLabel) { docType.label = docType.portalLabel; } @@ -60,7 +59,7 @@ export class CodeController { applicationTypes, noticeOfIntentTypes, submissionTypes, - applicationDocumentTypes: mappedDocTypes, + documentTypes: mappedDocTypes, naruSubtypes, }; } diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts new file mode 100644 index 0000000000..920aa7d08b --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts @@ -0,0 +1,179 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; +import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentProfile } from '../../common/automapper/notice-of-intent.automapper.profile'; +import { DocumentCode } from '../../document/document-code.entity'; +import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM } from '../../document/document.dto'; +import { Document } from '../../document/document.entity'; +import { DocumentService } from '../../document/document.service'; +import { User } from '../../user/user.entity'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission/notice-of-intent-submission.service'; +import { NoticeOfIntentDocumentController } from './notice-of-intent-document.controller'; +import { AttachExternalDocumentDto } from './notice-of-intent-document.dto'; + +describe('NoticeOfIntentDocumentController', () => { + let controller: NoticeOfIntentDocumentController; + let noiDocumentService: DeepMocked; + let mockNoiSubmissionService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockNoticeOfIntentService: DeepMocked; + + const mockDocument = new NoticeOfIntentDocument({ + document: new Document({ + fileName: 'fileName', + uploadedAt: new Date(), + uploadedBy: new User(), + }), + }); + + beforeEach(async () => { + noiDocumentService = createMock(); + mockDocumentService = createMock(); + mockNoiSubmissionService = createMock(); + mockNoticeOfIntentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentDocumentController], + providers: [ + NoticeOfIntentProfile, + { + provide: NoticeOfIntentDocumentService, + useValue: noiDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoiSubmissionService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: NoticeOfIntentService, + useValue: mockNoticeOfIntentService, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + controller = module.get( + NoticeOfIntentDocumentController, + ); + + mockNoiSubmissionService.verifyAccessByFileId.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + mockNoticeOfIntentService.getUuid.mockResolvedValue('uuid'); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call through to delete documents', async () => { + noiDocumentService.delete.mockResolvedValue(mockDocument); + noiDocumentService.get.mockResolvedValue(mockDocument); + + await controller.delete('fake-uuid', { + user: { + entity: {}, + }, + }); + + expect(noiDocumentService.get).toHaveBeenCalledTimes(1); + expect(noiDocumentService.delete).toHaveBeenCalledTimes(1); + }); + + it('should call through to update documents', async () => { + noiDocumentService.updateDescriptionAndType.mockResolvedValue([]); + + await controller.update( + 'file-number', + { + user: { + entity: {}, + }, + }, + [], + ); + + expect(noiDocumentService.updateDescriptionAndType).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should call through for download', async () => { + const fakeUrl = 'fake-url'; + noiDocumentService.getInlineUrl.mockResolvedValue(fakeUrl); + noiDocumentService.get.mockResolvedValue(mockDocument); + + const res = await controller.open('fake-uuid', { + user: { + entity: {}, + }, + }); + + expect(res.url).toEqual(fakeUrl); + }); + + it('should call out to service to attach external document', async () => { + const user = { user: { entity: 'Bruce' } }; + const fakeUuid = 'fakeUuid'; + const docObj = new Document({ uuid: 'fake-uuid' }); + const userEntity = new User({ + name: user.user.entity, + }); + + const docDto: AttachExternalDocumentDto = { + fileSize: 0, + mimeType: 'mimeType', + fileName: 'fileName', + fileKey: 'fileKey', + source: DOCUMENT_SOURCE.APPLICANT, + }; + + mockDocumentService.createDocumentRecord.mockResolvedValue(docObj); + + noiDocumentService.attachExternalDocument.mockResolvedValue( + new NoticeOfIntentDocument({ + noticeOfIntent: undefined, + type: new DocumentCode(), + uuid: fakeUuid, + document: new Document({ + uploadedAt: new Date(), + uploadedBy: userEntity, + }), + }), + ); + + const res = await controller.attachExternalDocument( + 'fake-number', + docDto, + user, + ); + + expect(mockDocumentService.createDocumentRecord).toBeCalledTimes(1); + expect(noiDocumentService.attachExternalDocument).toBeCalledTimes(1); + expect(mockDocumentService.createDocumentRecord).toBeCalledWith({ + ...docDto, + system: DOCUMENT_SYSTEM.PORTAL, + }); + expect(res.uploadedBy).toEqual(user.user.entity); + expect(res.uuid).toEqual(fakeUuid); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts new file mode 100644 index 0000000000..fb61df26e8 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts @@ -0,0 +1,162 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Req, + UseGuards, +} from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { ApplicationDocumentDto } from '../../alcs/application/application-document/application-document.dto'; +import { VISIBILITY_FLAG } from '../../alcs/application/application-document/application-document.entity'; +import { NoticeOfIntentDocumentDto } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; +import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; +import { DOCUMENT_SYSTEM } from '../../document/document.dto'; +import { DocumentService } from '../../document/document.service'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission/notice-of-intent-submission.service'; +import { + AttachExternalDocumentDto, + PortalNoticeOfIntentDocumentUpdateDto, +} from './notice-of-intent-document.dto'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(PortalAuthGuard) +@Controller('notice-of-intent-document') +export class NoticeOfIntentDocumentController { + constructor( + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private noticeOfIntentService: NoticeOfIntentService, + private documentService: DocumentService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('/application/:fileNumber') + async listApplicantDocuments( + @Param('fileNumber') fileNumber: string, + @Param('documentType') documentType: DOCUMENT_TYPE | null, + @Req() req, + ): Promise { + await this.noticeOfIntentSubmissionService.verifyAccessByFileId( + fileNumber, + req.user.entity, + ); + + const documents = await this.noticeOfIntentDocumentService.list( + fileNumber, + [VISIBILITY_FLAG.APPLICANT], + ); + return this.mapPortalDocuments(documents); + } + + @Get('/:uuid/open') + async open(@Param('uuid') fileUuid: string, @Req() req) { + const document = await this.noticeOfIntentDocumentService.get(fileUuid); + + //TODO: How do we know which documents applicant can access? + // await this.applicationSubmissionService.verifyAccess( + // document.applicationUuid, + // req.user.entity, + // ); + + const url = await this.noticeOfIntentDocumentService.getInlineUrl(document); + return { url }; + } + + @Patch('/application/:fileNumber') + async update( + @Param('fileNumber') fileNumber: string, + @Req() req, + @Body() body: PortalNoticeOfIntentDocumentUpdateDto[], + ) { + await this.noticeOfIntentSubmissionService.verifyAccessByFileId( + fileNumber, + req.user.entity, + ); + + //Map from file number to uuid + const noticeOfIntentUuid = await this.noticeOfIntentService.getUuid( + fileNumber, + ); + + const res = + await this.noticeOfIntentDocumentService.updateDescriptionAndType( + body, + noticeOfIntentUuid, + ); + return this.mapPortalDocuments(res); + } + + @Delete('/:uuid') + async delete(@Param('uuid') fileUuid: string, @Req() req) { + const document = await this.noticeOfIntentDocumentService.get(fileUuid); + + //TODO: How do we know which documents applicant can delete? + // await this.applicationSubmissionService.verifyAccess( + // document.applicationUuid, + // req.user.entity, + // ); + + await this.noticeOfIntentDocumentService.delete(document); + return {}; + } + + @Post('/application/:uuid/attachExternal') + async attachExternalDocument( + @Param('uuid') fileNumber: string, + @Body() data: AttachExternalDocumentDto, + @Req() req, + ): Promise { + const submission = + await this.noticeOfIntentSubmissionService.verifyAccessByFileId( + fileNumber, + req.user.entity, + ); + + const document = await this.documentService.createDocumentRecord({ + ...data, + system: DOCUMENT_SYSTEM.PORTAL, + }); + + const savedDocument = + await this.noticeOfIntentDocumentService.attachExternalDocument( + submission.fileNumber, + { + documentUuid: document.uuid, + type: data.documentType, + }, + [ + VISIBILITY_FLAG.APPLICANT, + VISIBILITY_FLAG.GOVERNMENT, + VISIBILITY_FLAG.COMMISSIONER, + ], + ); + + const mappedDocs = this.mapPortalDocuments([savedDocument]); + return mappedDocs[0]; + } + + private mapPortalDocuments(documents: NoticeOfIntentDocument[]) { + const labeledDocuments = documents.map((document) => { + if (document.type?.portalLabel) { + document.type.label = document.type.portalLabel; + } + return document; + }); + return this.mapper.mapArray( + labeledDocuments, + NoticeOfIntentDocument, + NoticeOfIntentDocumentDto, + ); + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.dto.ts new file mode 100644 index 0000000000..3cbf7d4a0e --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.dto.ts @@ -0,0 +1,30 @@ +import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; +import { DOCUMENT_SOURCE } from '../../document/document.dto'; + +export class AttachExternalDocumentDto { + @IsString() + mimeType: string; + + @IsString() + fileName: string; + + @IsNumber() + fileSize: number; + + @IsString() + fileKey: string; + + @IsString() + source: DOCUMENT_SOURCE.APPLICANT; + + @IsString() + @IsOptional() + documentType?: DOCUMENT_TYPE; +} + +export class PortalNoticeOfIntentDocumentUpdateDto { + uuid: string; + type: DOCUMENT_TYPE | null; + description: string | null; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.module.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.module.ts new file mode 100644 index 0000000000..ae4a6622a8 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { NoticeOfIntentModule } from '../../alcs/notice-of-intent/notice-of-intent.module'; +import { DocumentModule } from '../../document/document.module'; +import { NoticeOfIntentSubmissionModule } from '../notice-of-intent-submission/notice-of-intent-submission.module'; +import { NoticeOfIntentDocumentController } from './notice-of-intent-document.controller'; + +@Module({ + imports: [ + DocumentModule, + NoticeOfIntentModule, + NoticeOfIntentSubmissionModule, + ], + controllers: [NoticeOfIntentDocumentController], + providers: [], + exports: [], +}) +export class PortalNoticeOfIntentDocumentModule {} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.spec.ts new file mode 100644 index 0000000000..ee57a26100 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.spec.ts @@ -0,0 +1,345 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { BadRequestException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerProfile } from '../../../common/automapper/notice-of-intent-owner.automapper.profile'; +import { + OWNER_TYPE, + OwnerType, +} from '../../../common/owner-type/owner-type.entity'; +import { DocumentService } from '../../../document/document.service'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { NoticeOfIntentOwnerController } from './notice-of-intent-owner.controller'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner.entity'; +import { NoticeOfIntentOwnerService } from './notice-of-intent-owner.service'; + +describe('NoticeOfIntentOwnerController', () => { + let controller: NoticeOfIntentOwnerController; + let mockNOISubmissionService: DeepMocked; + let mockOwnerService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockNOIDocumentService: DeepMocked; + + beforeEach(async () => { + mockNOISubmissionService = createMock(); + mockOwnerService = createMock(); + mockDocumentService = createMock(); + mockNOIDocumentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentOwnerController], + providers: [ + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNOISubmissionService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockOwnerService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNOIDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + NoticeOfIntentOwnerProfile, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + NoticeOfIntentOwnerController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should verify access before fetching applications and map displayName', async () => { + const owner = new NoticeOfIntentOwner({ + firstName: 'Bruce', + lastName: 'Wayne', + }); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission({ + owners: [owner], + }), + ); + + const owners = await controller.fetchByFileId('', { + user: { + entity: {}, + }, + }); + + expect(owners.length).toEqual(1); + expect(owners[0].displayName).toBe('Bruce Wayne'); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should verify the dto and file access then create', async () => { + const owner = new NoticeOfIntentOwner({ + firstName: 'Bruce', + lastName: 'Wayne', + }); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + mockOwnerService.create.mockResolvedValue(owner); + + const createdOwner = await controller.create( + { + firstName: 'B', + lastName: 'W', + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: 'INDV', + }, + { + user: { + entity: {}, + }, + }, + ); + + expect(createdOwner).toBeDefined(); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.create).toHaveBeenCalledTimes(1); + }); + + it('should throw an exception when creating an individual owner without first name', async () => { + const promise = controller.create( + { + lastName: 'W', + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: 'INDV', + }, + { + user: { + entity: {}, + }, + }, + ); + await expect(promise).rejects.toMatchObject( + new BadRequestException('Individuals require both first and last name'), + ); + }); + + it('should throw an exception when creating an organization an org name', async () => { + const promise = controller.create( + { + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: 'ORGZ', + }, + { + user: { + entity: {}, + }, + }, + ); + await expect(promise).rejects.toMatchObject( + new BadRequestException('Organizations must have an organizationName'), + ); + }); + + it('should call through for update', async () => { + mockOwnerService.update.mockResolvedValue(new NoticeOfIntentOwner()); + mockOwnerService.getOwner.mockResolvedValue(new NoticeOfIntentOwner()); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + const res = await controller.update( + '', + { + organizationName: 'orgName', + email: '', + phoneNumber: '', + typeCode: 'ORGZ', + }, + { + user: { + entity: {}, + }, + }, + ); + + expect(mockOwnerService.update).toHaveBeenCalledTimes(1); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.getOwner).toHaveBeenCalledTimes(1); + }); + + it('should call through for delete', async () => { + mockOwnerService.delete.mockResolvedValue({} as any); + mockOwnerService.getOwner.mockResolvedValue(new NoticeOfIntentOwner()); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.delete('', { + user: { + entity: {}, + }, + }); + + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.delete).toHaveBeenCalledTimes(1); + expect(mockOwnerService.getOwner).toHaveBeenCalledTimes(1); + }); + + it('should call through for attachToParcel', async () => { + mockOwnerService.attachToParcel.mockResolvedValue({} as any); + mockOwnerService.getOwner.mockResolvedValue(new NoticeOfIntentOwner()); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.linkToParcel('', '', { + user: { + entity: {}, + }, + }); + + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.attachToParcel).toHaveBeenCalledTimes(1); + expect(mockOwnerService.getOwner).toHaveBeenCalledTimes(1); + }); + + it('should call through for removeFromParcel', async () => { + mockOwnerService.removeFromParcel.mockResolvedValue({} as any); + mockOwnerService.getOwner.mockResolvedValue(new NoticeOfIntentOwner()); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.removeFromParcel('', '', { + user: { + entity: {}, + }, + }); + + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.removeFromParcel).toHaveBeenCalledTimes(1); + expect(mockOwnerService.getOwner).toHaveBeenCalledTimes(1); + }); + + it('should create a new owner when setting primary contact to third party agent that doesnt exist', async () => { + mockOwnerService.deleteNonParcelOwners.mockResolvedValue([]); + mockOwnerService.create.mockResolvedValue(new NoticeOfIntentOwner()); + mockOwnerService.setPrimaryContact.mockResolvedValue(); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.setPrimaryContact( + { noticeOfIntentSubmissionUuid: '' }, + { + user: { + entity: {}, + }, + }, + ); + + expect(mockOwnerService.deleteNonParcelOwners).toHaveBeenCalledTimes(1); + expect(mockOwnerService.create).toHaveBeenCalledTimes(1); + expect(mockOwnerService.setPrimaryContact).toHaveBeenCalledTimes(1); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should set the owner and delete agents when using a non-agent owner', async () => { + mockOwnerService.getOwner.mockResolvedValue( + new NoticeOfIntentOwner({ + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, + }), + }), + ); + mockOwnerService.setPrimaryContact.mockResolvedValue(); + mockOwnerService.deleteNonParcelOwners.mockResolvedValue({} as any); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.setPrimaryContact( + { noticeOfIntentSubmissionUuid: '', ownerUuid: 'fake-uuid' }, + { + user: { + entity: {}, + }, + }, + ); + + expect(mockOwnerService.setPrimaryContact).toHaveBeenCalledTimes(1); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.deleteNonParcelOwners).toHaveBeenCalledTimes(1); + }); + + it('should update the agent owner when calling set primary contact', async () => { + mockOwnerService.getOwner.mockResolvedValue( + new NoticeOfIntentOwner({ + type: new OwnerType({ + code: OWNER_TYPE.AGENT, + }), + }), + ); + mockOwnerService.update.mockResolvedValue(new NoticeOfIntentOwner()); + mockOwnerService.setPrimaryContact.mockResolvedValue(); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.setPrimaryContact( + { noticeOfIntentSubmissionUuid: '', ownerUuid: 'fake-uuid' }, + { + user: { + entity: {}, + }, + }, + ); + + expect(mockOwnerService.setPrimaryContact).toHaveBeenCalledTimes(1); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.update).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.ts new file mode 100644 index 0000000000..981c11489f --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.ts @@ -0,0 +1,259 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Req, + UseGuards, +} from '@nestjs/common'; +import { VISIBILITY_FLAG } from '../../../alcs/application/application-document/application-document.entity'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { PortalAuthGuard } from '../../../common/authorization/portal-auth-guard.service'; +import { OWNER_TYPE } from '../../../common/owner-type/owner-type.entity'; +import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; +import { + DOCUMENT_SOURCE, + DOCUMENT_SYSTEM, +} from '../../../document/document.dto'; +import { DocumentService } from '../../../document/document.service'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { + AttachCorporateSummaryDto, + NoticeOfIntentOwnerCreateDto, + NoticeOfIntentOwnerDto, + NoticeOfIntentOwnerUpdateDto, + SetPrimaryContactDto, +} from './notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner.entity'; +import { NoticeOfIntentOwnerService } from './notice-of-intent-owner.service'; + +@Controller('notice-of-intent-owner') +@UseGuards(PortalAuthGuard) +export class NoticeOfIntentOwnerController { + constructor( + private ownerService: NoticeOfIntentOwnerService, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private documentService: DocumentService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('submission/:submissionUuid') + async fetchByFileId( + @Param('submissionUuid') submissionUuid: string, + @Req() req, + ): Promise { + const noticeOfIntentSubmission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + submissionUuid, + req.user.entity, + ); + + return this.mapper.mapArrayAsync( + noticeOfIntentSubmission.owners, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + } + + @Post() + async create( + @Body() createDto: NoticeOfIntentOwnerCreateDto, + @Req() req, + ): Promise { + this.verifyDto(createDto); + + const application = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + createDto.noticeOfIntentSubmissionUuid, + req.user.entity, + ); + const owner = await this.ownerService.create(createDto, application); + + return this.mapper.mapAsync( + owner, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + } + + @Patch('/:uuid') + async update( + @Param('uuid') uuid: string, + @Body() updateDto: NoticeOfIntentOwnerUpdateDto, + @Req() req, + ) { + await this.verifyAccessAndGetOwner(req, uuid); + this.verifyDto(updateDto); + + const newParcel = await this.ownerService.update(uuid, updateDto); + + return this.mapper.mapAsync( + newParcel, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + } + + @Delete('/:uuid') + async delete(@Param('uuid') uuid: string, @Req() req) { + const owner = await this.verifyAccessAndGetOwner(req, uuid); + if (owner.corporateSummary) { + await this.noticeOfIntentDocumentService.delete(owner.corporateSummary); + } + await this.ownerService.delete(owner); + return { uuid }; + } + + @Post('/:uuid/link/:parcelUuid') + async linkToParcel( + @Param('uuid') uuid: string, + @Param('parcelUuid') parcelUuid: string, + @Req() req, + ) { + await this.verifyAccessAndGetOwner(req, uuid); + + return { uuid: await this.ownerService.attachToParcel(uuid, parcelUuid) }; + } + + @Post('/:uuid/unlink/:parcelUuid') + async removeFromParcel( + @Param('uuid') uuid: string, + @Param('parcelUuid') parcelUuid: string, + @Req() req, + ) { + await this.verifyAccessAndGetOwner(req, uuid); + + return { uuid: await this.ownerService.removeFromParcel(uuid, parcelUuid) }; + } + + private verifyDto( + dto: NoticeOfIntentOwnerUpdateDto | NoticeOfIntentOwnerCreateDto, + ) { + if ( + dto.typeCode === OWNER_TYPE.INDIVIDUAL && + (!dto.firstName || !dto.lastName) + ) { + throw new BadRequestException( + 'Individuals require both first and last name', + ); + } + + if (dto.typeCode === OWNER_TYPE.ORGANIZATION && !dto.organizationName) { + throw new BadRequestException( + 'Organizations must have an organizationName', + ); + } + } + + @Post('setPrimaryContact') + async setPrimaryContact(@Body() data: SetPrimaryContactDto, @Req() req) { + const applicationSubmission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + data.noticeOfIntentSubmissionUuid, + req.user.entity, + ); + + //Create Owner + if (!data.ownerUuid) { + await this.ownerService.deleteNonParcelOwners(applicationSubmission.uuid); + const newOwner = await this.ownerService.create( + { + email: data.email, + typeCode: data.type, + lastName: data.lastName, + firstName: data.firstName, + phoneNumber: data.phoneNumber, + organizationName: data.organization, + noticeOfIntentSubmissionUuid: data.noticeOfIntentSubmissionUuid, + }, + applicationSubmission, + ); + await this.ownerService.setPrimaryContact( + applicationSubmission.uuid, + newOwner, + ); + } else if (data.ownerUuid) { + const primaryContactOwner = await this.ownerService.getOwner( + data.ownerUuid, + ); + + if ( + primaryContactOwner.type.code === OWNER_TYPE.AGENT || + primaryContactOwner.type.code === OWNER_TYPE.GOVERNMENT + ) { + //Update Fields for non parcel owners + await this.ownerService.update(primaryContactOwner.uuid, { + email: data.email, + typeCode: primaryContactOwner.type.code, + lastName: data.lastName, + firstName: data.firstName, + phoneNumber: data.phoneNumber, + organizationName: data.organization, + }); + } else { + //Delete Non parcel owners if we aren't using one + await this.ownerService.deleteNonParcelOwners( + applicationSubmission.uuid, + ); + } + + await this.ownerService.setPrimaryContact( + applicationSubmission.uuid, + primaryContactOwner, + ); + } + } + + private async verifyAccessAndGetOwner(@Req() req, ownerUuid: string) { + const owner = await this.ownerService.getOwner(ownerUuid); + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + owner.noticeOfIntentSubmissionUuid, + req.user.entity, + ); + + return owner; + } + + @Post('attachCorporateSummary') + async attachCorporateSummary( + @Req() req, + @Body() data: AttachCorporateSummaryDto, + ) { + await this.noticeOfIntentSubmissionService.verifyAccessByFileId( + data.fileNumber, + req.user.entity, + ); + + const document = await this.documentService.createDocumentRecord({ + ...data, + uploadedBy: req.user.entity, + source: DOCUMENT_SOURCE.APPLICANT, + system: DOCUMENT_SYSTEM.PORTAL, + }); + + const applicationDocument = + await this.noticeOfIntentDocumentService.attachExternalDocument( + data.fileNumber, + { + documentUuid: document.uuid, + type: DOCUMENT_TYPE.CORPORATE_SUMMARY, + }, + [ + VISIBILITY_FLAG.APPLICANT, + VISIBILITY_FLAG.GOVERNMENT, + VISIBILITY_FLAG.COMMISSIONER, + ], + ); + + return { + uuid: applicationDocument.uuid, + }; + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts new file mode 100644 index 0000000000..08ab45b2ea --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts @@ -0,0 +1,138 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsNumber, + IsOptional, + IsString, + IsUUID, + Matches, +} from 'class-validator'; +import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { + OWNER_TYPE, + OwnerTypeDto, +} from '../../../common/owner-type/owner-type.entity'; +import { emailRegex } from '../../../utils/email.helper'; +import { NoticeOfIntentParcelDto } from '../notice-of-intent-parcel/notice-of-intent-parcel.dto'; + +export class NoticeOfIntentOwnerDto { + @AutoMap() + uuid: string; + + @AutoMap() + noticeOfIntentSubmissionUuid: string; + + @AutoMap(() => String) + corporateSummaryUuid?: string; + + displayName: string; + + @AutoMap(() => String) + firstName?: string | null; + + @AutoMap(() => String) + lastName?: string | null; + + @AutoMap(() => String) + organizationName?: string | null; + + @AutoMap(() => String) + phoneNumber?: string | null; + + @AutoMap(() => String) + email?: string | null; + + @AutoMap() + type: OwnerTypeDto; + + @AutoMap(() => NoticeOfIntentDocumentDto) + corporateSummary?: NoticeOfIntentDocumentDto; +} + +export class NoticeOfIntentOwnerDetailedDto extends NoticeOfIntentOwnerDto { + parcels: NoticeOfIntentParcelDto[]; +} + +export class NoticeOfIntentOwnerUpdateDto { + @IsString() + @IsOptional() + firstName?: string; + + @IsString() + @IsOptional() + lastName?: string; + + @IsString() + @IsOptional() + organizationName?: string; + + @IsString() + @IsOptional() + phoneNumber?: string; + + @Matches(emailRegex) + @IsOptional() + email?: string; + + @IsString() + @IsOptional() + typeCode?: string; + + @IsUUID() + @IsOptional() + corporateSummaryUuid?: string; +} + +export class NoticeOfIntentOwnerCreateDto extends NoticeOfIntentOwnerUpdateDto { + @IsString() + noticeOfIntentSubmissionUuid: string; +} + +export class SetPrimaryContactDto { + @IsString() + @IsOptional() + firstName?: string; + + @IsString() + @IsOptional() + lastName?: string; + + @IsString() + @IsOptional() + organization?: string; + + @IsString() + @IsOptional() + phoneNumber?: string; + + @IsString() + @IsOptional() + email?: string; + + @IsString() + @IsOptional() + type?: OWNER_TYPE; + + @IsUUID() + @IsOptional() + ownerUuid?: string; + + @IsString() + noticeOfIntentSubmissionUuid: string; +} + +export class AttachCorporateSummaryDto { + @IsString() + mimeType: string; + + @IsString() + fileName: string; + + @IsNumber() + fileSize: number; + + @IsString() + fileKey: string; + + @IsString() + fileNumber: string; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity.ts new file mode 100644 index 0000000000..62ec03a981 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity.ts @@ -0,0 +1,77 @@ +import { AutoMap } from '@automapper/classes'; +import { Column, Entity, JoinColumn, ManyToMany, ManyToOne } from 'typeorm'; +import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocument } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { Base } from '../../../common/entities/base.entity'; +import { OwnerType } from '../../../common/owner-type/owner-type.entity'; +import { NoticeOfIntentParcel } from '../notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; + +@Entity() +export class NoticeOfIntentOwner extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + firstName?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + lastName?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + organizationName?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + phoneNumber?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + email?: string | null; + + @AutoMap(() => NoticeOfIntentDocumentDto) + @ManyToOne(() => NoticeOfIntentDocument, { + onDelete: 'SET NULL', + }) + @JoinColumn() + corporateSummary: NoticeOfIntentDocument | null; + + @Column({ nullable: true }) + corporateSummaryUuid: string | null; + + @AutoMap() + @ManyToOne(() => OwnerType, { nullable: false }) + type: OwnerType; + + @ManyToOne(() => NoticeOfIntentSubmission, { nullable: false }) + noticeOfIntentSubmission: NoticeOfIntentSubmission; + + @AutoMap() + @Column() + noticeOfIntentSubmissionUuid: string; + + @ManyToMany(() => NoticeOfIntentParcel, (appParcel) => appParcel.owners) + parcels: NoticeOfIntentParcel[]; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts new file mode 100644 index 0000000000..9493d13e28 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts @@ -0,0 +1,341 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ApplicationDocumentService } from '../../../alcs/application/application-document/application-document.service'; +import { NoticeOfIntentDocument } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { OwnerType } from '../../../common/owner-type/owner-type.entity'; +import { NoticeOfIntentParcel } from '../notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from '../notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner.entity'; +import { NoticeOfIntentOwnerService } from './notice-of-intent-owner.service'; + +describe('NoticeOfIntentOwnerService', () => { + let service: NoticeOfIntentOwnerService; + let mockParcelService: DeepMocked; + let mockRepo: DeepMocked>; + let mockTypeRepo: DeepMocked>; + let mockAppDocumentService: DeepMocked; + let mockApplicationservice: DeepMocked; + + beforeEach(async () => { + mockParcelService = createMock(); + mockRepo = createMock(); + mockTypeRepo = createMock(); + mockAppDocumentService = createMock(); + mockApplicationservice = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeOfIntentOwnerService, + { + provide: NoticeOfIntentParcelService, + useValue: mockParcelService, + }, + { + provide: getRepositoryToken(NoticeOfIntentOwner), + useValue: mockRepo, + }, + { + provide: getRepositoryToken(OwnerType), + useValue: mockTypeRepo, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockAppDocumentService, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockApplicationservice, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentOwnerService, + ); + + mockParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([ + new NoticeOfIntentParcel({ + owners: [new NoticeOfIntentOwner()], + }), + ]); + mockApplicationservice.update.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should call find for find', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + + await service.fetchByApplicationFileId(''); + + expect(mockRepo.find).toHaveBeenCalledTimes(1); + }); + + it('should load the type and then call save for create', async () => { + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + mockTypeRepo.findOneOrFail.mockResolvedValue(new OwnerType()); + + await service.create( + { + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: '', + }, + new NoticeOfIntentSubmission(), + ); + + expect(mockRepo.save).toHaveBeenCalledTimes(1); + expect(mockTypeRepo.findOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should load the type/parcel and then call save for attachToParcel', async () => { + const owner = new NoticeOfIntentOwner({ + parcels: [], + }); + mockRepo.findOneOrFail.mockResolvedValue(owner); + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + mockParcelService.getOneOrFail.mockResolvedValue( + new NoticeOfIntentParcel(), + ); + + await service.attachToParcel('', ''); + + expect(owner.parcels.length).toEqual(1); + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepo.save).toHaveBeenCalledTimes(1); + expect(mockParcelService.getOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should remove the parcel from the array then call save for removeFromParcel', async () => { + const parcelUuid = '1'; + const owner = new NoticeOfIntentOwner({ + parcels: [ + new NoticeOfIntentParcel({ + uuid: parcelUuid, + }), + ], + }); + mockRepo.findOneOrFail.mockResolvedValue(owner); + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + + await service.removeFromParcel('', parcelUuid); + + expect(owner.parcels.length).toEqual(0); + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepo.save).toHaveBeenCalledTimes(1); + }); + + it('should set properties and call save for update', async () => { + const owner = new NoticeOfIntentOwner({ + firstName: 'Bruce', + lastName: 'Wayne', + }); + mockRepo.findOneOrFail.mockResolvedValue(owner); + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + + await service.update('', { + firstName: 'I Am', + lastName: 'Batman', + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(owner.firstName).toEqual('I Am'); + expect(owner.lastName).toEqual('Batman'); + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepo.save).toHaveBeenCalledTimes(1); + }); + + it('should delete the existing document when updating', async () => { + const owner = new NoticeOfIntentOwner({ + firstName: 'Bruce', + lastName: 'Wayne', + corporateSummaryUuid: 'oldUuid', + corporateSummary: new NoticeOfIntentDocument(), + }); + mockRepo.findOneOrFail.mockResolvedValue(owner); + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + mockAppDocumentService.delete.mockResolvedValue({} as any); + + await service.update('', { + organizationName: '', + email: '', + phoneNumber: '', + typeCode: '', + corporateSummaryUuid: 'newUuid', + }); + + expect(owner.corporateSummaryUuid).toEqual('newUuid'); + expect(mockAppDocumentService.delete).toHaveBeenCalledTimes(1); + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepo.save).toHaveBeenCalledTimes(2); + }); + + it('should call through for delete', async () => { + mockRepo.remove.mockResolvedValue({} as any); + + await service.delete(new NoticeOfIntentOwner()); + + expect(mockRepo.remove).toHaveBeenCalledTimes(1); + }); + + it('should call through for verify', async () => { + mockRepo.findOneOrFail.mockResolvedValue(new NoticeOfIntentOwner()); + + await service.getOwner(''); + + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should call through for getMany', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + + await service.getMany([]); + + expect(mockRepo.find).toHaveBeenCalledTimes(1); + }); + + it('should call through for save', async () => { + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + + await service.save(new NoticeOfIntentOwner()); + + expect(mockRepo.save).toHaveBeenCalledTimes(1); + }); + + it('should call update for the application with the first parcels last name', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + const owners = [ + new NoticeOfIntentOwner({ + firstName: 'B', + lastName: 'A', + }), + ]; + mockParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([ + new NoticeOfIntentParcel({ + owners, + }), + ]); + + await service.updateSubmissionApplicant(''); + + expect(mockApplicationservice.update).toHaveBeenCalledTimes(1); + expect(mockApplicationservice.update.mock.calls[0][1].applicant).toEqual( + 'A', + ); + }); + + it('should call update for the application with the first parcels last name', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + const owners = [ + new NoticeOfIntentOwner({ + firstName: 'F', + lastName: 'B', + }), + new NoticeOfIntentOwner({ + firstName: 'F', + lastName: 'A', + }), + new NoticeOfIntentOwner({ + firstName: 'F', + lastName: '1', + }), + new NoticeOfIntentOwner({ + firstName: 'F', + lastName: 'C', + }), + ]; + mockParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([ + new NoticeOfIntentParcel({ + owners, + }), + ]); + + await service.updateSubmissionApplicant(''); + + expect(mockApplicationservice.update).toHaveBeenCalledTimes(1); + expect(mockApplicationservice.update.mock.calls[0][1].applicant).toEqual( + 'A et al.', + ); + }); + + it('should call update for the application with the number owners last name', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + const owners = [ + new NoticeOfIntentOwner({ + firstName: '1', + lastName: '1', + }), + new NoticeOfIntentOwner({ + firstName: '2', + lastName: '2', + }), + ]; + mockParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([ + new NoticeOfIntentParcel({ + owners, + }), + ]); + + await service.updateSubmissionApplicant(''); + + expect(mockApplicationservice.update).toHaveBeenCalledTimes(1); + expect(mockApplicationservice.update.mock.calls[0][1].applicant).toEqual( + '1 et al.', + ); + }); + + it('should use the first created parcel to set the application applicants name', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + const owners1 = [ + new NoticeOfIntentOwner({ + firstName: 'C', + lastName: 'C', + }), + ]; + const owners2 = [ + new NoticeOfIntentOwner({ + firstName: 'A', + lastName: 'A', + }), + ]; + mockParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([ + new NoticeOfIntentParcel({ + owners: owners1, + auditCreatedAt: new Date(1), + }), + new NoticeOfIntentParcel({ + owners: owners2, + auditCreatedAt: new Date(100), + }), + ]); + + await service.updateSubmissionApplicant(''); + + expect(mockApplicationservice.update).toHaveBeenCalledTimes(1); + expect(mockApplicationservice.update.mock.calls[0][1].applicant).toEqual( + 'A et al.', + ); + }); + + it('should load then delete non application owners', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + mockRepo.remove.mockResolvedValue([] as any); + + await service.deleteNonParcelOwners('uuid'); + + expect(mockRepo.find).toHaveBeenCalledTimes(1); + expect(mockRepo.remove).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.ts new file mode 100644 index 0000000000..33d81961a7 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.ts @@ -0,0 +1,294 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Any, Repository } from 'typeorm'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { + OWNER_TYPE, + OwnerType, +} from '../../../common/owner-type/owner-type.entity'; +import { NoticeOfIntentParcelService } from '../notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { + NoticeOfIntentOwnerCreateDto, + NoticeOfIntentOwnerUpdateDto, +} from './notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner.entity'; + +@Injectable() +export class NoticeOfIntentOwnerService { + constructor( + @InjectRepository(NoticeOfIntentOwner) + private repository: Repository, + @InjectRepository(OwnerType) + private typeRepository: Repository, + @Inject(forwardRef(() => NoticeOfIntentParcelService)) + private noticeOfIntentParcelService: NoticeOfIntentParcelService, + @Inject(forwardRef(() => NoticeOfIntentSubmissionService)) + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + ) {} + + async fetchByApplicationFileId(fileId: string) { + return this.repository.find({ + where: { + noticeOfIntentSubmission: { + fileNumber: fileId, + }, + }, + relations: { + type: true, + corporateSummary: { + document: true, + }, + }, + }); + } + + async create( + createDto: NoticeOfIntentOwnerCreateDto, + noticeOfIntentSubmission: NoticeOfIntentSubmission, + ) { + const type = await this.typeRepository.findOneOrFail({ + where: { + code: createDto.typeCode, + }, + }); + + const newOwner = new NoticeOfIntentOwner({ + firstName: createDto.firstName, + lastName: createDto.lastName, + organizationName: createDto.organizationName, + email: createDto.email, + phoneNumber: createDto.phoneNumber, + corporateSummaryUuid: createDto.corporateSummaryUuid, + noticeOfIntentSubmission, + type, + }); + + return await this.repository.save(newOwner); + } + + async attachToParcel(uuid: string, parcelUuid: string) { + const existingOwner = await this.repository.findOneOrFail({ + where: { + uuid, + }, + relations: { + parcels: true, + }, + }); + + const parcel = await this.noticeOfIntentParcelService.getOneOrFail( + parcelUuid, + ); + existingOwner.parcels.push(parcel); + + await this.updateSubmissionApplicant( + existingOwner.noticeOfIntentSubmissionUuid, + ); + + await this.repository.save(existingOwner); + } + + async save(owner: NoticeOfIntentOwner) { + await this.repository.save(owner); + } + + async removeFromParcel(uuid: string, parcelUuid: string) { + const existingOwner = await this.repository.findOneOrFail({ + where: { + uuid, + }, + relations: { + parcels: true, + }, + }); + + existingOwner.parcels = existingOwner.parcels.filter( + (parcel) => parcel.uuid !== parcelUuid, + ); + + await this.updateSubmissionApplicant( + existingOwner.noticeOfIntentSubmissionUuid, + ); + + await this.repository.save(existingOwner); + } + + async update(uuid: string, updateDto: NoticeOfIntentOwnerUpdateDto) { + const existingOwner = await this.repository.findOneOrFail({ + where: { + uuid, + }, + relations: { + corporateSummary: { + document: true, + }, + }, + }); + + if (updateDto.typeCode) { + existingOwner.type = await this.typeRepository.findOneOrFail({ + where: { + code: updateDto.typeCode, + }, + }); + } + + //If attaching new document and old one was defined, delete it + if ( + existingOwner.corporateSummaryUuid !== updateDto.corporateSummaryUuid && + existingOwner.corporateSummary + ) { + const oldSummary = existingOwner.corporateSummary; + existingOwner.corporateSummary = null; + await this.repository.save(existingOwner); + await this.noticeOfIntentDocumentService.delete(oldSummary); + } + + existingOwner.corporateSummaryUuid = + updateDto.corporateSummaryUuid !== undefined + ? updateDto.corporateSummaryUuid + : existingOwner.corporateSummaryUuid; + + existingOwner.organizationName = + updateDto.organizationName !== undefined + ? updateDto.organizationName + : existingOwner.organizationName; + + existingOwner.firstName = + updateDto.firstName !== undefined + ? updateDto.firstName + : existingOwner.firstName; + + existingOwner.lastName = + updateDto.lastName !== undefined + ? updateDto.lastName + : existingOwner.lastName; + + existingOwner.phoneNumber = + updateDto.phoneNumber !== undefined + ? updateDto.phoneNumber + : existingOwner.phoneNumber; + + existingOwner.email = + updateDto.email !== undefined ? updateDto.email : existingOwner.email; + + await this.updateSubmissionApplicant( + existingOwner.noticeOfIntentSubmissionUuid, + ); + + return await this.repository.save(existingOwner); + } + + async delete(owner: NoticeOfIntentOwner) { + const res = await this.repository.remove(owner); + await this.updateSubmissionApplicant(owner.noticeOfIntentSubmissionUuid); + return res; + } + + async setPrimaryContact(submissionUuid: string, owner: NoticeOfIntentOwner) { + await this.noticeOfIntentSubmissionService.setPrimaryContact( + submissionUuid, + owner.uuid, + ); + } + + async getOwner(ownerUuid: string) { + return await this.repository.findOneOrFail({ + where: { + uuid: ownerUuid, + }, + relations: { + type: true, + corporateSummary: { + document: true, + }, + }, + }); + } + + async getMany(ownerUuids: string[]) { + return await this.repository.find({ + where: { + uuid: Any(ownerUuids), + }, + }); + } + + async deleteNonParcelOwners(submissionUuid: string) { + const agentOwners = await this.repository.find({ + where: [ + { + noticeOfIntentSubmission: { + uuid: submissionUuid, + }, + type: { + code: OWNER_TYPE.AGENT, + }, + }, + { + noticeOfIntentSubmission: { + uuid: submissionUuid, + }, + type: { + code: OWNER_TYPE.GOVERNMENT, + }, + }, + ], + }); + return await this.repository.remove(agentOwners); + } + + async updateSubmissionApplicant(submissionUuid: string) { + const parcels = + await this.noticeOfIntentParcelService.fetchByApplicationSubmissionUuid( + submissionUuid, + ); + + if (parcels.length > 0) { + const firstParcel = parcels.reduce((a, b) => + a.auditCreatedAt > b.auditCreatedAt ? a : b, + ); + + const ownerCount = parcels.reduce((count, parcel) => { + return count + parcel.owners.length; + }, 0); + + if (firstParcel) { + //Filter to only alphabetic + const alphabetOwners = firstParcel.owners.filter((owner) => + isNaN( + parseInt( + (owner.organizationName ?? owner.lastName ?? '').charAt(0), + ), + ), + ); + + //If no alphabetic use them all + if (alphabetOwners.length === 0) { + alphabetOwners.push(...firstParcel.owners); + } + + const firstOwner = alphabetOwners.sort((a, b) => { + const mappedA = a.organizationName ?? a.lastName ?? ''; + const mappedB = b.organizationName ?? b.lastName ?? ''; + return mappedA.localeCompare(mappedB); + })[0]; + if (firstOwner) { + let applicantName = firstOwner.organizationName + ? firstOwner.organizationName + : firstOwner.lastName; + if (ownerCount > 1) { + applicantName += ' et al.'; + } + + await this.noticeOfIntentSubmissionService.update(submissionUuid, { + applicant: applicantName || '', + }); + } + } + } + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts new file mode 100644 index 0000000000..e6d3229bd5 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts @@ -0,0 +1,5 @@ +import { Entity } from 'typeorm'; +import { BaseCodeEntity } from '../../../../common/entities/base.code.entity'; + +@Entity() +export class NoticeOfIntentParcelOwnershipType extends BaseCodeEntity {} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts new file mode 100644 index 0000000000..b2cbd3aa93 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts @@ -0,0 +1,165 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentParcelProfile } from '../../../common/automapper/notice-of-intent-parcel.automapper.profile'; +import { DocumentService } from '../../../document/document.service'; +import { NoticeOfIntentOwnerService } from '../notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { NoticeOfIntentParcelController } from './notice-of-intent-parcel.controller'; +import { NoticeOfIntentParcelUpdateDto } from './notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel.service'; + +describe('NoticeOfIntentParcelController', () => { + let controller: NoticeOfIntentParcelController; + let mockNOIParcelService: DeepMocked; + let mockNOIService: DeepMocked; + let mockNOIOwnerService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockNOIDocumentService: DeepMocked; + + beforeEach(async () => { + mockNOIParcelService = createMock(); + mockNOIService = createMock(); + mockNOIOwnerService = createMock(); + mockDocumentService = createMock(); + mockNOIDocumentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentParcelController], + providers: [ + NoticeOfIntentParcelProfile, + { + provide: NoticeOfIntentParcelService, + useValue: mockNOIParcelService, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNOIService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockNOIOwnerService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNOIDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + NoticeOfIntentParcelController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call out to service when fetching parcels', async () => { + mockNOIParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([]); + + const parcels = await controller.fetchByFileId('mockFileID'); + + expect(parcels).toBeDefined(); + expect( + mockNOIParcelService.fetchByApplicationSubmissionUuid, + ).toHaveBeenCalledTimes(1); + }); + + it('should call out to service when creating parcels', async () => { + mockNOIService.getOrFailByUuid.mockResolvedValue( + {} as NoticeOfIntentSubmission, + ); + mockNOIParcelService.create.mockResolvedValue({} as NoticeOfIntentParcel); + mockNOIOwnerService.attachToParcel.mockResolvedValue(); + + const parcel = await controller.create({ + noticeOfIntentSubmissionUuid: 'fake', + }); + + expect(mockNOIService.getOrFailByUuid).toBeCalledTimes(1); + expect(mockNOIParcelService.create).toBeCalledTimes(1); + expect(mockNOIOwnerService.attachToParcel).toBeCalledTimes(0); + expect(parcel).toBeDefined(); + }); + + it('should call out to service and revert newly created "other" parcel if failed to link it to and owner during creation process', async () => { + const mockError = new Error('mock error'); + mockNOIService.getOrFailByUuid.mockResolvedValue( + {} as NoticeOfIntentSubmission, + ); + mockNOIParcelService.create.mockResolvedValue({} as NoticeOfIntentParcel); + mockNOIOwnerService.attachToParcel.mockRejectedValue(mockError); + mockNOIParcelService.deleteMany.mockResolvedValue([]); + + await expect( + controller.create({ + noticeOfIntentSubmissionUuid: 'fake', + ownerUuid: 'fake_uuid', + parcelType: 'other', + }), + ).rejects.toMatchObject(mockError); + + expect(mockNOIService.getOrFailByUuid).toBeCalledTimes(1); + expect(mockNOIParcelService.create).toBeCalledTimes(1); + expect(mockNOIParcelService.deleteMany).toBeCalledTimes(1); + expect(mockNOIOwnerService.attachToParcel).toBeCalledTimes(1); + }); + + it('should call out to service when updating parcel', async () => { + const mockUpdateDto: NoticeOfIntentParcelUpdateDto[] = [ + { + uuid: 'fake_uuid', + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legal', + mapAreaHectares: 2, + purchasedDate: 1, + isFarm: true, + isConfirmedByApplicant: true, + ownershipTypeCode: 'SMPL', + ownerUuids: null, + }, + ]; + + mockNOIParcelService.update.mockResolvedValue([ + {}, + ] as NoticeOfIntentParcel[]); + + const parcel = await controller.update(mockUpdateDto); + + expect(mockNOIParcelService.update).toBeCalledTimes(1); + expect(parcel).toBeDefined(); + }); + + it('should call out to service when deleting parcel', async () => { + const fakeUuid = 'fake_uuid'; + mockNOIParcelService.deleteMany.mockResolvedValue([]); + + const result = await controller.delete([fakeUuid]); + + expect(mockNOIParcelService.deleteMany).toBeCalledTimes(1); + expect(result).toBeDefined(); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts new file mode 100644 index 0000000000..d84a44e651 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts @@ -0,0 +1,158 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; +import { VISIBILITY_FLAG } from '../../../alcs/application/application-document/application-document.entity'; +import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocument } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { PortalAuthGuard } from '../../../common/authorization/portal-auth-guard.service'; +import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; +import { + DOCUMENT_SOURCE, + DOCUMENT_SYSTEM, +} from '../../../document/document.dto'; +import { DocumentService } from '../../../document/document.service'; +import { NoticeOfIntentOwnerService } from '../notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { + AttachCertificateOfTitleDto, + NoticeOfIntentParcelCreateDto, + NoticeOfIntentParcelDto, + NoticeOfIntentParcelUpdateDto, +} from './notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel.service'; + +@Controller('notice-of-intent-parcel') +@UseGuards(PortalAuthGuard) +export class NoticeOfIntentParcelController { + constructor( + private parcelService: NoticeOfIntentParcelService, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + @InjectMapper() private mapper: Mapper, + private ownerService: NoticeOfIntentOwnerService, + private documentService: DocumentService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + ) {} + + @Get('submission/:submissionUuid') + async fetchByFileId( + @Param('submissionUuid') submissionUuid: string, + ): Promise { + const parcels = await this.parcelService.fetchByApplicationSubmissionUuid( + submissionUuid, + ); + return this.mapper.mapArrayAsync( + parcels, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + + @Post() + async create( + @Body() createDto: NoticeOfIntentParcelCreateDto, + ): Promise { + const noticeOfIntentSubmission = + await this.noticeOfIntentSubmissionService.getOrFailByUuid( + createDto.noticeOfIntentSubmissionUuid, + ); + const parcel = await this.parcelService.create( + noticeOfIntentSubmission.uuid, + ); + + try { + if (createDto.ownerUuid) { + await this.ownerService.attachToParcel( + createDto.ownerUuid, + parcel.uuid, + ); + } + } catch (e) { + await this.delete([parcel.uuid]); + throw e; + } + + return this.mapper.mapAsync( + parcel, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + + @Put('/') + async update( + @Body() updateDtos: NoticeOfIntentParcelUpdateDto[], + ): Promise { + const updatedParcels = await this.parcelService.update(updateDtos); + + return this.mapper.mapArrayAsync( + updatedParcels, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + + @Delete() + async delete(@Body() uuids: string[]) { + const deletedParcels = await this.parcelService.deleteMany(uuids); + return this.mapper.mapArrayAsync( + deletedParcels, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + + @Post(':uuid/attachCertificateOfTitle') + async attachCorporateSummary( + @Req() req, + @Param('uuid') parcelUuid: string, + @Body() data: AttachCertificateOfTitleDto, + ) { + const parcel = await this.parcelService.getOneOrFail(parcelUuid); + const document = await this.documentService.createDocumentRecord({ + ...data, + uploadedBy: req.user.entity, + source: DOCUMENT_SOURCE.APPLICANT, + system: DOCUMENT_SYSTEM.PORTAL, + }); + + const noticeOfIntentSubmission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + parcel.noticeOfIntentSubmissionUuid, + req.user.entity, + ); + + const certificateOfTitle = + await this.noticeOfIntentDocumentService.attachExternalDocument( + noticeOfIntentSubmission!.fileNumber, + { + documentUuid: document.uuid, + type: DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + }, + [ + VISIBILITY_FLAG.APPLICANT, + VISIBILITY_FLAG.GOVERNMENT, + VISIBILITY_FLAG.COMMISSIONER, + ], + ); + + await this.parcelService.setCertificateOfTitle(parcel, certificateOfTitle); + + return this.mapper.map( + certificateOfTitle, + NoticeOfIntentDocument, + NoticeOfIntentDocumentDto, + ); + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts new file mode 100644 index 0000000000..2e6732dd14 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts @@ -0,0 +1,147 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsArray, + IsBoolean, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; +import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { BaseCodeDto } from '../../../common/dtos/base.dto'; +import { NoticeOfIntentOwnerDto } from '../notice-of-intent-owner/notice-of-intent-owner.dto'; + +export class NoticeOfIntentParcelOwnershipTypeDto extends BaseCodeDto {} + +export class NoticeOfIntentParcelDto { + @AutoMap() + uuid: string; + + @AutoMap() + applicationSubmissionUuid: string; + + @AutoMap(() => String) + certificateOfTitleUuid: string | null; + + @AutoMap(() => String) + pid?: string | null; + + @AutoMap(() => String) + pin?: string | null; + + @AutoMap(() => String) + legalDescription?: string | null; + + @AutoMap(() => String) + civicAddress?: string | null; + + @AutoMap(() => Number) + mapAreaHectares?: number | null; + + @AutoMap(() => Number) + purchasedDate?: number | null; + + @AutoMap(() => Boolean) + isFarm?: boolean | null; + + @AutoMap(() => Boolean) + isConfirmedByApplicant?: boolean; + + @AutoMap(() => String) + ownershipTypeCode?: string | null; + + @AutoMap(() => String) + crownLandOwnerType?: string | null; + + ownershipType?: NoticeOfIntentParcelOwnershipTypeDto; + + @AutoMap(() => String) + parcelType: string; + + @AutoMap(() => Number) + alrArea: number | null; + + certificateOfTitle?: NoticeOfIntentDocumentDto; + owners: NoticeOfIntentOwnerDto[]; +} + +export class NoticeOfIntentParcelCreateDto { + @IsNotEmpty() + @IsString() + noticeOfIntentSubmissionUuid: string; + + @IsOptional() + @IsString() + parcelType?: string; + + @IsOptional() + @IsString() + ownerUuid?: string; +} + +export class NoticeOfIntentParcelUpdateDto { + @IsString() + uuid: string; + + @IsString() + @IsOptional() + pid?: string | null; + + @IsString() + @IsOptional() + pin?: string | null; + + @IsString() + @IsOptional() + civicAddress?: string | null; + + @IsString() + @IsOptional() + legalDescription?: string | null; + + @IsNumber() + @IsOptional() + mapAreaHectares?: number | null; + + @IsNumber() + @IsOptional() + purchasedDate?: number | null; + + @IsBoolean() + @IsOptional() + isFarm?: boolean | null; + + @IsBoolean() + @IsOptional() + isConfirmedByApplicant?: boolean; + + @IsString() + @IsOptional() + ownershipTypeCode?: string | null; + + @IsString() + @IsOptional() + crownLandOwnerType?: string | null; + + @IsArray() + @IsOptional() + ownerUuids?: string[] | null; + + @IsNumber() + @IsOptional() + alrArea?: number | null; +} + +export class AttachCertificateOfTitleDto { + @IsString() + mimeType: string; + + @IsString() + fileName: string; + + @IsNumber() + fileSize: number; + + @IsString() + fileKey: string; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts new file mode 100644 index 0000000000..12b817142c --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts @@ -0,0 +1,147 @@ +import { AutoMap } from '@automapper/classes'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, +} from 'typeorm'; +import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocument } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { Base } from '../../../common/entities/base.entity'; +import { ColumnNumericTransformer } from '../../../utils/column-numeric-transform'; +import { NoticeOfIntentOwner } from '../notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; +import { NoticeOfIntentParcelOwnershipType } from './notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity'; + +@Entity() +export class NoticeOfIntentParcel extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: + 'The Parcels pid entered by the user or populated from third-party data', + nullable: true, + }) + pid?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: + 'The Parcels pin entered by the user or populated from third-party data', + nullable: true, + }) + pin?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: + 'The Parcels legalDescription entered by the user or populated from third-party data', + nullable: true, + }) + legalDescription?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: 'The standard address for the parcel', + nullable: true, + }) + civicAddress?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'float', + comment: + 'The Parcels map are in hectares entered by the user or populated from third-party data', + nullable: true, + }) + mapAreaHectares?: number | null; + + @AutoMap(() => String) + @Column({ + type: 'boolean', + comment: 'The Parcels indication whether it is used as a farm', + nullable: true, + }) + isFarm?: boolean | null; + + @AutoMap() + @Column({ + type: 'timestamptz', + nullable: true, + comment: 'The Parcels purchase date provided by user', + }) + purchasedDate?: Date | null; + + @AutoMap(() => Boolean) + @Column({ + type: 'boolean', + comment: + 'The Parcels indication whether applicant signed off provided data including the Certificate of Title', + nullable: false, + default: false, + }) + isConfirmedByApplicant: boolean; + + @AutoMap() + @ManyToOne(() => NoticeOfIntentSubmission) + noticeOfIntentSubmission: NoticeOfIntentSubmission; + + @AutoMap() + @Column() + noticeOfIntentSubmissionUuid: string; + + @AutoMap(() => String) + @Column({ nullable: true }) + ownershipTypeCode?: string | null; + + @AutoMap() + @ManyToOne(() => NoticeOfIntentParcelOwnershipType) + ownershipType: NoticeOfIntentParcelOwnershipType; + + @AutoMap(() => Boolean) + @Column({ + type: 'text', + comment: + 'For Crown Land parcels to indicate whether they are provincially owned or federally owned', + nullable: true, + }) + crownLandOwnerType?: string | null; + + @ManyToMany(() => NoticeOfIntentOwner, (owner) => owner.parcels) + @JoinTable() + owners: NoticeOfIntentOwner[]; + + @AutoMap(() => NoticeOfIntentDocumentDto) + @JoinColumn() + @ManyToOne(() => NoticeOfIntentDocument, { + nullable: true, + onDelete: 'SET NULL', + }) + certificateOfTitle?: NoticeOfIntentDocument; + + @AutoMap(() => String) + @Column({ nullable: true }) + certificateOfTitleUuid: string | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + alrArea?: number | null; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts new file mode 100644 index 0000000000..099481434d --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts @@ -0,0 +1,248 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { NoticeOfIntentOwnerService } from '../notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelUpdateDto } from './notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel.service'; + +describe('NoticeOfIntentParcelService', () => { + let service: NoticeOfIntentParcelService; + let mockParcelRepo: DeepMocked>; + let mockOwnerService: DeepMocked; + + const mockFileNumber = 'mock_applicationFileNumber'; + const mockUuid = 'mock_uuid'; + const mockNOIParcel = new NoticeOfIntentParcel({ + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isFarm: true, + purchasedDate: new Date(1, 1, 1), + isConfirmedByApplicant: true, + noticeOfIntentSubmissionUuid: mockFileNumber, + ownershipTypeCode: 'mock_ownershipTypeCode', + }); + const mockError = new Error('Parcel does not exist.'); + + beforeEach(async () => { + mockParcelRepo = createMock(); + mockOwnerService = createMock(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeOfIntentParcelService, + { + provide: getRepositoryToken(NoticeOfIntentParcel), + useValue: mockParcelRepo, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockOwnerService, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentParcelService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should fetch parcels by fileNumber', async () => { + mockParcelRepo.find.mockResolvedValue([mockNOIParcel]); + + const result = await service.fetchByApplicationFileId(mockFileNumber); + + expect(result).toEqual([mockNOIParcel]); + expect(mockParcelRepo.find).toBeCalledTimes(1); + expect(mockParcelRepo.find).toBeCalledWith({ + where: { + noticeOfIntentSubmission: { + fileNumber: mockFileNumber, + isDraft: false, + }, + }, + order: { auditCreatedAt: 'ASC' }, + relations: { + certificateOfTitle: { document: true }, + owners: { + corporateSummary: { + document: true, + }, + type: true, + }, + ownershipType: true, + }, + }); + }); + + it('should get one parcel by id', async () => { + mockParcelRepo.findOneOrFail.mockResolvedValue(mockNOIParcel); + + const result = await service.getOneOrFail(mockUuid); + + expect(result).toEqual(mockNOIParcel); + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + }); + + it('should raise error on get parcel by uuid if the parcel does not exist', async () => { + mockParcelRepo.findOneOrFail.mockRejectedValue(mockError); + + await expect(service.getOneOrFail(mockUuid)).rejects.toMatchObject( + mockError, + ); + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + }); + + it('should successfully update parcel', async () => { + const updateParcelDto = [ + { + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isFarm: true, + purchasedDate: 1, + isConfirmedByApplicant: true, + ownershipTypeCode: 'mock_ownershipTypeCode', + }, + ] as NoticeOfIntentParcelUpdateDto[]; + + mockParcelRepo.findOneOrFail.mockResolvedValue(mockNOIParcel); + mockParcelRepo.save.mockResolvedValue({} as NoticeOfIntentParcel); + + await service.update(updateParcelDto); + + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + expect(mockParcelRepo.save).toBeCalledTimes(1); + }); + + it('should update the applicant if the parcel has owners', async () => { + const updateParcelDto = [ + { + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isFarm: true, + purchasedDate: 1, + isConfirmedByApplicant: true, + ownershipTypeCode: 'mock_ownershipTypeCode', + ownerUuids: ['cats'], + }, + ] as NoticeOfIntentParcelUpdateDto[]; + + mockParcelRepo.findOneOrFail.mockResolvedValue(mockNOIParcel); + mockParcelRepo.save.mockResolvedValue({} as NoticeOfIntentParcel); + mockOwnerService.updateSubmissionApplicant.mockResolvedValue(); + mockOwnerService.getMany.mockResolvedValue([]); + + await service.update(updateParcelDto); + + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + expect(mockParcelRepo.save).toBeCalledTimes(1); + expect(mockOwnerService.updateSubmissionApplicant).toHaveBeenCalledTimes(1); + }); + + it('it should fail to update a parcel if the parcel does not exist. ', async () => { + const updateParcelDto: NoticeOfIntentParcelUpdateDto[] = [ + { + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isFarm: true, + purchasedDate: 1, + isConfirmedByApplicant: true, + ownershipTypeCode: 'mock_ownershipTypeCode', + }, + ]; + const mockError = new Error('Parcel does not exist.'); + + mockParcelRepo.findOneOrFail.mockRejectedValue(mockError); + mockParcelRepo.save.mockResolvedValue(new NoticeOfIntentParcel()); + + await expect(service.update(updateParcelDto)).rejects.toMatchObject( + mockError, + ); + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + expect(mockParcelRepo.save).toBeCalledTimes(0); + }); + + it('should successfully delete a parcel and update applicant', async () => { + mockParcelRepo.find.mockResolvedValue([mockNOIParcel]); + mockParcelRepo.remove.mockResolvedValue(new NoticeOfIntentParcel()); + mockOwnerService.updateSubmissionApplicant.mockResolvedValue(); + + const result = await service.deleteMany([mockUuid]); + + expect(result).toBeDefined(); + expect(mockParcelRepo.find).toBeCalledTimes(1); + expect(mockParcelRepo.find).toBeCalledWith({ + where: { uuid: In([mockUuid]) }, + }); + expect(mockParcelRepo.remove).toBeCalledWith([mockNOIParcel]); + expect(mockParcelRepo.remove).toBeCalledTimes(1); + expect(mockOwnerService.updateSubmissionApplicant).toHaveBeenCalledTimes(1); + }); + + it('should not call remove if the parcel does not exist', async () => { + const exception = new ServiceValidationException( + `Unable to find parcels with provided uuids: ${mockUuid}.`, + ); + + mockParcelRepo.find.mockResolvedValue([]); + mockParcelRepo.remove.mockResolvedValue(new NoticeOfIntentParcel()); + + await expect(service.deleteMany([mockUuid])).rejects.toMatchObject( + exception, + ); + expect(mockParcelRepo.find).toBeCalledTimes(1); + expect(mockParcelRepo.find).toBeCalledWith({ + where: { uuid: In([mockUuid]) }, + }); + expect(mockParcelRepo.remove).toBeCalledTimes(0); + }); + + it('should successfully create a parcel', async () => { + mockParcelRepo.save.mockResolvedValue({ + uuid: mockUuid, + noticeOfIntentSubmissionUuid: mockFileNumber, + } as NoticeOfIntentParcel); + + const mockParcel = new NoticeOfIntentParcel({ + uuid: mockUuid, + noticeOfIntentSubmissionUuid: mockFileNumber, + }); + + const result = await service.create(mockFileNumber); + + expect(result).toEqual(mockParcel); + expect(mockParcelRepo.save).toBeCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts new file mode 100644 index 0000000000..ba69dc46bb --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts @@ -0,0 +1,140 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { NoticeOfIntentDocument } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { formatIncomingDate } from '../../../utils/incoming-date.formatter'; +import { filterUndefined } from '../../../utils/undefined'; +import { NoticeOfIntentOwnerService } from '../notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelUpdateDto } from './notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel.entity'; + +@Injectable() +export class NoticeOfIntentParcelService { + constructor( + @InjectRepository(NoticeOfIntentParcel) + private parcelRepository: Repository, + @Inject(forwardRef(() => NoticeOfIntentOwnerService)) + private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + ) {} + + async fetchByApplicationFileId(fileId: string) { + return this.parcelRepository.find({ + where: { + noticeOfIntentSubmission: { fileNumber: fileId, isDraft: false }, + }, + order: { auditCreatedAt: 'ASC' }, + relations: { + ownershipType: true, + certificateOfTitle: { document: true }, + owners: { + type: true, + corporateSummary: { + document: true, + }, + }, + }, + }); + } + + async fetchByApplicationSubmissionUuid(uuid: string) { + return this.parcelRepository.find({ + where: { noticeOfIntentSubmission: { uuid } }, + order: { auditCreatedAt: 'ASC' }, + relations: { + ownershipType: true, + certificateOfTitle: { document: true }, + owners: { + type: true, + corporateSummary: { + document: true, + }, + }, + }, + }); + } + + async create(noticeOfIntentSubmissionUuid: string) { + const parcel = new NoticeOfIntentParcel({ + noticeOfIntentSubmissionUuid, + }); + + return this.parcelRepository.save(parcel); + } + + async getOneOrFail(uuid: string) { + return this.parcelRepository.findOneOrFail({ + where: { uuid }, + }); + } + + async setCertificateOfTitle( + parcel: NoticeOfIntentParcel, + certificateOfTitle: NoticeOfIntentDocument, + ) { + parcel.certificateOfTitle = certificateOfTitle; + return await this.parcelRepository.save(parcel); + } + + async update(updateDtos: NoticeOfIntentParcelUpdateDto[]) { + const updatedParcels: NoticeOfIntentParcel[] = []; + + let hasOwnerUpdate = false; + for (const updateDto of updateDtos) { + const parcel = await this.getOneOrFail(updateDto.uuid); + + parcel.pid = updateDto.pid; + parcel.pin = updateDto.pin; + parcel.legalDescription = updateDto.legalDescription; + parcel.mapAreaHectares = updateDto.mapAreaHectares; + parcel.civicAddress = updateDto.civicAddress; + parcel.isFarm = updateDto.isFarm; + parcel.purchasedDate = formatIncomingDate(updateDto.purchasedDate); + parcel.ownershipTypeCode = updateDto.ownershipTypeCode; + parcel.isConfirmedByApplicant = filterUndefined( + updateDto.isConfirmedByApplicant, + parcel.isConfirmedByApplicant, + ); + parcel.crownLandOwnerType = updateDto.crownLandOwnerType; + parcel.alrArea = updateDto.alrArea; + + if (updateDto.ownerUuids) { + hasOwnerUpdate = true; + parcel.owners = await this.noticeOfIntentOwnerService.getMany( + updateDto.ownerUuids, + ); + } + + updatedParcels.push(parcel); + } + + const res = await this.parcelRepository.save(updatedParcels); + + if (hasOwnerUpdate) { + const firstParcel = updatedParcels[0]; + await this.noticeOfIntentOwnerService.updateSubmissionApplicant( + firstParcel.noticeOfIntentSubmissionUuid, + ); + } + return res; + } + + async deleteMany(uuids: string[]) { + const parcels = await this.parcelRepository.find({ + where: { uuid: In(uuids) }, + }); + + if (parcels.length === 0) { + throw new ServiceValidationException( + `Unable to find parcels with provided uuids: ${uuids}.`, + ); + } + + const result = await this.parcelRepository.remove(parcels); + await this.noticeOfIntentOwnerService.updateSubmissionApplicant( + parcels[0].noticeOfIntentSubmissionUuid, + ); + + return result; + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts index 790ccad385..498b9a0389 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts @@ -12,6 +12,7 @@ import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; import { Base } from '../../common/entities/base.entity'; import { User } from '../../user/user.entity'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; @Entity() export class NoticeOfIntentSubmission extends Base { @@ -187,6 +188,12 @@ export class NoticeOfIntentSubmission extends Base { }) noticeOfIntent: NoticeOfIntent; + @OneToMany( + () => NoticeOfIntentOwner, + (owner) => owner.noticeOfIntentSubmission, + ) + owners: NoticeOfIntentOwner[]; + @OneToMany( () => NoticeOfIntentSubmissionToSubmissionStatus, (status) => status.submission, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts index 2a7378251c..33a9ac799f 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts @@ -5,16 +5,32 @@ import { LocalGovernmentModule } from '../../alcs/local-government/local-governm import { NoticeOfIntentSubmissionStatusModule } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.module'; import { NoticeOfIntentModule } from '../../alcs/notice-of-intent/notice-of-intent.module'; import { AuthorizationModule } from '../../common/authorization/authorization.module'; +import { NoticeOfIntentOwnerProfile } from '../../common/automapper/notice-of-intent-owner.automapper.profile'; +import { NoticeOfIntentParcelProfile } from '../../common/automapper/notice-of-intent-parcel.automapper.profile'; import { NoticeOfIntentSubmissionProfile } from '../../common/automapper/notice-of-intent-submission.automapper.profile'; +import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; +import { NoticeOfIntentOwnerController } from './notice-of-intent-owner/notice-of-intent-owner.controller'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentOwnerService } from './notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelOwnershipType } from './notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity'; +import { NoticeOfIntentParcelController } from './notice-of-intent-parcel/notice-of-intent-parcel.controller'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel/notice-of-intent-parcel.service'; import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission.controller'; import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; @Module({ imports: [ - TypeOrmModule.forFeature([NoticeOfIntentSubmission]), + TypeOrmModule.forFeature([ + NoticeOfIntentSubmission, + NoticeOfIntentParcel, + NoticeOfIntentParcelOwnershipType, + OwnerType, + NoticeOfIntentOwner, + ]), NoticeOfIntentSubmissionStatusModule, forwardRef(() => NoticeOfIntentModule), AuthorizationModule, @@ -23,7 +39,19 @@ import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.s LocalGovernmentModule, FileNumberModule, ], - controllers: [NoticeOfIntentSubmissionController], - providers: [NoticeOfIntentSubmissionService, NoticeOfIntentSubmissionProfile], + controllers: [ + NoticeOfIntentSubmissionController, + NoticeOfIntentParcelController, + NoticeOfIntentOwnerController, + ], + providers: [ + NoticeOfIntentSubmissionService, + NoticeOfIntentParcelService, + NoticeOfIntentOwnerService, + NoticeOfIntentSubmissionProfile, + NoticeOfIntentOwnerProfile, + NoticeOfIntentParcelProfile, + ], + exports: [NoticeOfIntentSubmissionService], }) export class NoticeOfIntentSubmissionModule {} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index abf85de386..db15158ca5 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -408,4 +408,8 @@ export class NoticeOfIntentSubmissionService { }, }); } + + async setPrimaryContact(submissionUuid: string, uuid: any) { + //TODO:? ?? + } } diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts index 68992e01a4..6d087bfb97 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts @@ -10,6 +10,7 @@ import { import * as config from 'config'; import * as dayjs from 'dayjs'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { OWNER_TYPE } from '../../common/owner-type/owner-type.entity'; import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocument, @@ -20,7 +21,6 @@ import { ApplicationService } from '../../alcs/application/application.service'; import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM } from '../../document/document.dto'; import { User } from '../../user/user.entity'; import { formatBooleanToYesNoString } from '../../utils/boolean-formatter'; -import { APPLICATION_OWNER } from '../application-submission/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../application-submission/application-owner/application-owner.service'; import { PARCEL_TYPE } from '../application-submission/application-parcel/application-parcel.dto'; import { ApplicationParcel } from '../application-submission/application-parcel/application-parcel.entity'; @@ -232,7 +232,7 @@ export class GenerateSubmissionDocumentService { applicant: submission.applicant, primaryContact, organizationText: - primaryContact?.type.code === APPLICATION_OWNER.CROWN + primaryContact?.type.code === OWNER_TYPE.CROWN ? 'Ministry/Department Responsible' : 'Organization (If Applicable)', diff --git a/services/apps/alcs/src/portal/portal.module.ts b/services/apps/alcs/src/portal/portal.module.ts index 4db394b272..8612f010e2 100644 --- a/services/apps/alcs/src/portal/portal.module.ts +++ b/services/apps/alcs/src/portal/portal.module.ts @@ -12,6 +12,7 @@ import { ApplicationSubmissionReviewModule } from './application-submission-revi import { ApplicationSubmissionModule } from './application-submission/application-submission.module'; import { CodeController } from './code/code.controller'; import { PortalDocumentModule } from './document/document.module'; +import { PortalNoticeOfIntentDocumentModule } from './notice-of-intent-document/notice-of-intent-document.module'; import { NoticeOfIntentSubmissionModule } from './notice-of-intent-submission/notice-of-intent-submission.module'; import { ParcelModule } from './parcel/parcel.module'; import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; @@ -32,6 +33,7 @@ import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; PortalApplicationDecisionModule, NoticeOfIntentModule, NoticeOfIntentSubmissionModule, + PortalNoticeOfIntentDocumentModule, RouterModule.register([ { path: 'portal', module: ApplicationSubmissionModule }, { path: 'portal', module: NoticeOfIntentSubmissionModule }, @@ -42,6 +44,7 @@ import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; { path: 'portal', module: ApplicationSubmissionDraftModule }, { path: 'portal', module: PdfGenerationModule }, { path: 'portal', module: PortalApplicationDecisionModule }, + { path: 'portal', module: PortalNoticeOfIntentDocumentModule }, ]), ], controllers: [CodeController], diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691692339215-add_noi_submission_parcels_and_owners.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691692339215-add_noi_submission_parcels_and_owners.ts new file mode 100644 index 0000000000..8547130c8c --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691692339215-add_noi_submission_parcels_and_owners.ts @@ -0,0 +1,111 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNoiSubmissionParcelsAndOwners1691692339215 + implements MigrationInterface +{ + name = 'addNoiSubmissionParcelsAndOwners1691692339215'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_owner" DROP CONSTRAINT "FK_05181ec6491ee0aa527bd55c714"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_owner_type" RENAME TO "owner_type"`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_parcel_ownership_type" ("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, "label" character varying NOT NULL, "code" text NOT NULL, "description" text NOT NULL, CONSTRAINT "UQ_a7d503e16b2d7a04680dbfdcb59" UNIQUE ("description"), CONSTRAINT "PK_7b23cfdc8574f66dd2f88e37f3f" PRIMARY KEY ("code"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_parcel" ("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(), "pid" character varying, "pin" character varying, "legal_description" character varying, "civic_address" character varying, "map_area_hectares" double precision, "is_farm" boolean, "purchased_date" TIMESTAMP WITH TIME ZONE, "is_confirmed_by_applicant" boolean NOT NULL DEFAULT false, "notice_of_intent_submission_uuid" uuid NOT NULL, "ownership_type_code" text, "crown_land_owner_type" text, "certificate_of_title_uuid" uuid, "alr_area" numeric(12,2), CONSTRAINT "PK_6cfd592c4237000793b5a891aa7" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."pid" IS 'The Parcels pid entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."pin" IS 'The Parcels pin entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."legal_description" IS 'The Parcels legalDescription entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."civic_address" IS 'The standard address for the parcel'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."map_area_hectares" IS 'The Parcels map are in hectares entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."is_farm" IS 'The Parcels indication whether it is used as a farm'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."purchased_date" IS 'The Parcels purchase date provided by user'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."is_confirmed_by_applicant" IS 'The Parcels indication whether applicant signed off provided data including the Certificate of Title'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."crown_land_owner_type" IS 'For Crown Land parcels to indicate whether they are provincially owned or federally owned'`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_owner" ("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(), "first_name" character varying, "last_name" character varying, "organization_name" character varying, "phone_number" character varying, "email" character varying, "corporate_summary_uuid" uuid, "notice_of_intent_submission_uuid" uuid NOT NULL, "type_code" text NOT NULL, CONSTRAINT "PK_057d19217ea35bb20b4b7e7ed43" PRIMARY KEY ("uuid"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" ("notice_of_intent_parcel_uuid" uuid NOT NULL, "notice_of_intent_owner_uuid" uuid NOT NULL, CONSTRAINT "PK_62cbd3e4f1802f240c40b1c37f8" PRIMARY KEY ("notice_of_intent_parcel_uuid", "notice_of_intent_owner_uuid"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_a419bb29b4bdb7e5d4aa4181d1" ON "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" ("notice_of_intent_parcel_uuid") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_4346ccc22b3ca8dafe57b25128" ON "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" ("notice_of_intent_owner_uuid") `, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_owner" ADD CONSTRAINT "FK_05181ec6491ee0aa527bd55c714" FOREIGN KEY ("type_code") REFERENCES "alcs"."owner_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" ADD CONSTRAINT "FK_8d14440209af146e7f351c7fbef" FOREIGN KEY ("notice_of_intent_submission_uuid") REFERENCES "alcs"."notice_of_intent_submission"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" ADD CONSTRAINT "FK_7b23cfdc8574f66dd2f88e37f3f" FOREIGN KEY ("ownership_type_code") REFERENCES "alcs"."notice_of_intent_parcel_ownership_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" ADD CONSTRAINT "FK_7b405f7700af3398ec38b70d634" FOREIGN KEY ("certificate_of_title_uuid") REFERENCES "alcs"."notice_of_intent_document"("uuid") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" ADD CONSTRAINT "FK_a27c5e1999ea94ce0dc0c923fb3" FOREIGN KEY ("corporate_summary_uuid") REFERENCES "alcs"."notice_of_intent_document"("uuid") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" ADD CONSTRAINT "FK_016562c9faf2b0d0ccc96212ee0" FOREIGN KEY ("type_code") REFERENCES "alcs"."owner_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" ADD CONSTRAINT "FK_df0c33df4e8ce83089e0645e928" FOREIGN KEY ("notice_of_intent_submission_uuid") REFERENCES "alcs"."notice_of_intent_submission"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" ADD CONSTRAINT "FK_a419bb29b4bdb7e5d4aa4181d11" FOREIGN KEY ("notice_of_intent_parcel_uuid") REFERENCES "alcs"."notice_of_intent_parcel"("uuid") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" ADD CONSTRAINT "FK_4346ccc22b3ca8dafe57b251280" FOREIGN KEY ("notice_of_intent_owner_uuid") REFERENCES "alcs"."notice_of_intent_owner"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" DROP CONSTRAINT "FK_4346ccc22b3ca8dafe57b251280"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" DROP CONSTRAINT "FK_a419bb29b4bdb7e5d4aa4181d11"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" DROP CONSTRAINT "FK_df0c33df4e8ce83089e0645e928"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" DROP CONSTRAINT "FK_016562c9faf2b0d0ccc96212ee0"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" DROP CONSTRAINT "FK_a27c5e1999ea94ce0dc0c923fb3"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" DROP CONSTRAINT "FK_7b405f7700af3398ec38b70d634"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" DROP CONSTRAINT "FK_7b23cfdc8574f66dd2f88e37f3f"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" DROP CONSTRAINT "FK_8d14440209af146e7f351c7fbef"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_owner" DROP CONSTRAINT "FK_05181ec6491ee0aa527bd55c714"`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_4346ccc22b3ca8dafe57b25128"`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_a419bb29b4bdb7e5d4aa4181d1"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner"`, + ); + await queryRunner.query(`DROP TABLE "alcs"."notice_of_intent_owner"`); + await queryRunner.query(`DROP TABLE "alcs"."notice_of_intent_parcel"`); + await queryRunner.query( + `DROP TABLE "alcs"."notice_of_intent_parcel_ownership_type"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."owner_type" RENAME TO "application_owner_type"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_owner" ADD CONSTRAINT "FK_05181ec6491ee0aa527bd55c714" FOREIGN KEY ("type_code") REFERENCES "alcs"."application_owner_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691705084117-seed_noi_parcel_owner_type.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691705084117-seed_noi_parcel_owner_type.ts new file mode 100644 index 0000000000..a400727ffd --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691705084117-seed_noi_parcel_owner_type.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class seedNoiParcelOwnerType1691705084117 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO "alcs"."notice_of_intent_parcel_ownership_type" + ("audit_deleted_date_at", "audit_created_at", "audit_updated_at", "audit_created_by", "audit_updated_by", "label", "code", "description") VALUES + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Crown', 'CRWN', 'Crown'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Fee Simple', 'SMPL', 'Fee Simple'); + `); + } + + public async down(queryRunner: QueryRunner): Promise {} +} From 4c8eda790135f7ffa3204aef27bedd6e76477b10 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 10:04:42 -0700 Subject: [PATCH 2/5] Code Review Feedback --- .../edit-submission/parcels/parcel-details.component.html | 8 ++++---- .../application-parcel/application-parcel.service.ts | 5 +++-- .../notice-of-intent-document.service.ts | 2 +- .../notice-of-intent-owner/notice-of-intent-owner.dto.ts | 8 ++++---- .../notice-of-intent-owner.service.ts | 8 ++++---- .../notice-of-intent-owner.service.spec.ts | 8 ++++---- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html index 0a42627959..b2b1ca3ae7 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html @@ -1,7 +1,7 @@
-

Identify Parcels Under Application

-

Provide parcel identification and registered ownership information for each parcel under application.

+

Identify Parcels Under Notice of Intent

+

Provide parcel identification and registered ownership information for each parcel.

*All fields are required unless stated optional.

Documents needed for this step:
@@ -23,7 +23,7 @@
Documents needed for this step:
Documents needed for this step:
- +
diff --git a/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts b/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts index 1112a20315..061f34564a 100644 --- a/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts +++ b/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts @@ -7,7 +7,7 @@ import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spin import { ApplicationDocumentDto } from '../application-document/application-document.dto'; import { DocumentService } from '../document/document.service'; import { ToastService } from '../toast/toast.service'; -import { ApplicationParcelDto, ApplicationParcelUpdateDto } from './application-parcel.dto'; +import { ApplicationParcelDto, ApplicationParcelUpdateDto, PARCEL_TYPE } from './application-parcel.dto'; @Injectable({ providedIn: 'root', @@ -34,11 +34,12 @@ export class ApplicationParcelService { return undefined; } - async create(applicationSubmissionUuid: string, ownerUuid?: string) { + async create(applicationSubmissionUuid: string, parcelType?: PARCEL_TYPE, ownerUuid?: string) { try { return await firstValueFrom( this.httpClient.post(`${this.serviceUrl}`, { applicationSubmissionUuid, + parcelType, ownerUuid, }) ); diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts index 6b51562e5e..7ae8f404a9 100644 --- a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts @@ -39,7 +39,7 @@ export class NoticeOfIntentDocumentService { return res; } catch (e) { console.error(e); - this.toastService.showErrorToast('Failed to attach document to Application, please try again'); + this.toastService.showErrorToast('Failed to attach document, please try again'); } return undefined; } diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts index 21b23da93c..48cf7cee32 100644 --- a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts @@ -1,6 +1,6 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; -import { ApplicationDocumentDto } from '../application-document/application-document.dto'; -import { ApplicationParcelDto } from '../application-parcel/application-parcel.dto'; +import { NoticeOfIntentDocumentDto } from '../notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentParcelDto } from '../notice-of-intent-parcel/notice-of-intent-parcel.dto'; export enum OWNER_TYPE { INDIVIDUAL = 'INDV', @@ -24,11 +24,11 @@ export interface NoticeOfIntentOwnerDto { phoneNumber: string | null; email: string | null; type: OwnerTypeDto; - corporateSummary?: ApplicationDocumentDto; + corporateSummary?: NoticeOfIntentDocumentDto; } export interface NoticeOfIntentOwnerDetailedDto extends NoticeOfIntentOwnerDto { - parcels: ApplicationParcelDto[]; + parcels: NoticeOfIntentParcelDto[]; } export interface NoticeOfIntentOwnerUpdateDto { diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts index 92abf5b9e6..66a48b3aab 100644 --- a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts @@ -67,11 +67,11 @@ export class NoticeOfIntentOwnerService { const res = await firstValueFrom( this.httpClient.post(`${this.serviceUrl}/setPrimaryContact`, updateDto) ); - this.toastService.showSuccessToast('Application saved'); + this.toastService.showSuccessToast('Notice of Intent saved'); return res; } catch (e) { console.error(e); - this.toastService.showErrorToast('Failed to update Application, please try again later'); + this.toastService.showErrorToast('Failed to update Notice of Intent, please try again later'); return undefined; } } @@ -126,10 +126,10 @@ export class NoticeOfIntentOwnerService { return 0; } - async uploadCorporateSummary(applicationFileId: string, file: File) { + async uploadCorporateSummary(noticeOfIntentFileId: string, file: File) { try { return await this.documentService.uploadFile<{ uuid: string }>( - applicationFileId, + noticeOfIntentFileId, file, DOCUMENT_TYPE.CORPORATE_SUMMARY, DOCUMENT_SOURCE.APPLICANT, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts index 9493d13e28..99eb1b68a1 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts @@ -214,7 +214,7 @@ describe('NoticeOfIntentOwnerService', () => { expect(mockRepo.save).toHaveBeenCalledTimes(1); }); - it('should call update for the application with the first parcels last name', async () => { + it('should call update with the first parcels last name', async () => { mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); const owners = [ new NoticeOfIntentOwner({ @@ -236,7 +236,7 @@ describe('NoticeOfIntentOwnerService', () => { ); }); - it('should call update for the application with the first parcels last name', async () => { + it('should call update with the first parcels last name', async () => { mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); const owners = [ new NoticeOfIntentOwner({ @@ -270,7 +270,7 @@ describe('NoticeOfIntentOwnerService', () => { ); }); - it('should call update for the application with the number owners last name', async () => { + it('should call update with the number owners last name', async () => { mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); const owners = [ new NoticeOfIntentOwner({ @@ -296,7 +296,7 @@ describe('NoticeOfIntentOwnerService', () => { ); }); - it('should use the first created parcel to set the application applicants name', async () => { + it('should use the first created parcel to set the applicants name', async () => { mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); const owners1 = [ new NoticeOfIntentOwner({ From aeeba9a78248df29782d415cffefffd58b44b3bc Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 11:10:34 -0700 Subject: [PATCH 3/5] Finish Implementing Owners * Genericize owner dialogs to work for both NOI and App --- .../edit-submission-base.module.ts | 12 -- .../application-owners-dialog.component.ts | 41 ------- .../parcel-entry/parcel-entry.component.html | 4 + .../parcel-entry/parcel-entry.component.ts | 33 ++++-- .../parcel-owners.component.html | 73 ------------ .../parcel-owners/parcel-owners.component.ts | 91 --------------- .../edit-submission-base.module.ts | 3 - .../parcels/parcel-details.component.ts | 12 +- .../parcel-entry/parcel-entry.component.html | 26 +++-- .../parcel-entry/parcel-entry.component.ts | 23 ++-- .../parcel-owners.component.scss | 14 --- .../parcel-owners.component.spec.ts | 41 ------- .../parcel-owners/parcel-owners.component.ts | 91 --------------- .../application-owner.service.ts | 10 -- .../crown-owner-dialog.component.html} | 0 .../crown-owner-dialog.component.scss} | 4 +- .../crown-owner-dialog.component.spec.ts} | 12 +- .../crown-owner-dialog.component.ts} | 35 +++--- .../owner-dialog/owner-dialog.component.html} | 0 .../owner-dialog/owner-dialog.component.scss} | 4 +- .../owner-dialog.component.spec.ts} | 16 +-- .../owner-dialog/owner-dialog.component.ts} | 46 ++++---- .../all-owners-dialog.component.html} | 2 + .../all-owners-dialog.component.scss} | 0 .../all-owners-dialog.component.spec.ts} | 12 +- .../all-owners-dialog.component.ts | 47 ++++++++ .../parcel-owners.component.html | 2 +- .../parcel-owners.component.scss | 4 +- .../parcel-owners.component.spec.ts | 4 +- .../parcel-owners/parcel-owners.component.ts | 110 ++++++++++++++++++ .../src/app/shared/shared.module.ts | 21 ++++ ...of-intent-submission.automapper.profile.ts | 16 +++ .../notice-of-intent-submission.dto.ts | 5 +- .../notice-of-intent-submission.service.ts | 8 +- 34 files changed, 343 insertions(+), 479 deletions(-) delete mode 100644 portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts delete mode 100644 portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html delete mode 100644 portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts delete mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss delete mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts delete mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html => shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.html} (100%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss => shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.scss} (72%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts => shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.spec.ts} (67%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts => shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.ts} (64%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html => shared/owner-dialogs/owner-dialog/owner-dialog.component.html} (100%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss => shared/owner-dialogs/owner-dialog/owner-dialog.component.scss} (75%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts => shared/owner-dialogs/owner-dialog/owner-dialog.component.spec.ts} (69%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts => shared/owner-dialogs/owner-dialog/owner-dialog.component.ts} (76%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html => shared/owner-dialogs/owners-dialog/all-owners-dialog.component.html} (82%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss => shared/owner-dialogs/owners-dialog/all-owners-dialog.component.scss} (100%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts => shared/owner-dialogs/owners-dialog/all-owners-dialog.component.spec.ts} (66%) create mode 100644 portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.ts rename portal-frontend/src/app/{features/notice-of-intents/edit-submission/parcels => shared/owner-dialogs}/parcel-owners/parcel-owners.component.html (97%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details => shared/owner-dialogs}/parcel-owners/parcel-owners.component.scss (66%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details => shared/owner-dialogs}/parcel-owners/parcel-owners.component.spec.ts (82%) create mode 100644 portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.ts diff --git a/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts index c70691f53a..ad2d84a82b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts @@ -15,14 +15,10 @@ import { LandUseComponent } from './land-use/land-use.component'; import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { OtherParcelConfirmationDialogComponent } from './other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component'; import { OtherParcelsComponent } from './other-parcels/other-parcels.component'; -import { ApplicationCrownOwnerDialogComponent } from './parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component'; -import { ApplicationOwnerDialogComponent } from './parcel-details/application-owner-dialog/application-owner-dialog.component'; -import { ApplicationOwnersDialogComponent } from './parcel-details/application-owners-dialog/application-owners-dialog.component'; import { DeleteParcelDialogComponent } from './parcel-details/delete-parcel/delete-parcel-dialog.component'; import { ParcelDetailsComponent } from './parcel-details/parcel-details.component'; import { ParcelEntryConfirmationDialogComponent } from './parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { ParcelEntryComponent } from './parcel-details/parcel-entry/parcel-entry.component'; -import { ParcelOwnersComponent } from './parcel-details/parcel-owners/parcel-owners.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; import { ExclProposalComponent } from './proposal/excl-proposal/excl-proposal.component'; import { InclProposalComponent } from './proposal/incl-proposal/incl-proposal.component'; @@ -48,10 +44,6 @@ import { SelectGovernmentComponent } from './select-government/select-government EditSubmissionComponent, DeleteParcelDialogComponent, SelectGovernmentComponent, - ParcelOwnersComponent, - ApplicationOwnersDialogComponent, - ApplicationOwnerDialogComponent, - ApplicationCrownOwnerDialogComponent, LandUseComponent, OtherParcelsComponent, OtherAttachmentsComponent, @@ -92,10 +84,6 @@ import { SelectGovernmentComponent } from './select-government/select-government EditSubmissionComponent, DeleteParcelDialogComponent, SelectGovernmentComponent, - ParcelOwnersComponent, - ApplicationOwnersDialogComponent, - ApplicationOwnerDialogComponent, - ApplicationCrownOwnerDialogComponent, LandUseComponent, OtherParcelsComponent, OtherAttachmentsComponent, diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts deleted file mode 100644 index 8a884734aa..0000000000 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; - -@Component({ - selector: 'app-application-owner-dialog', - templateUrl: './application-owners-dialog.component.html', - styleUrls: ['./application-owners-dialog.component.scss'], -}) -export class ApplicationOwnersDialogComponent { - isDirty = false; - owners: ApplicationOwnerDto[] = []; - fileId: string; - submissionUuid: string; - - constructor( - private applicationOwnerService: ApplicationOwnerService, - @Inject(MAT_DIALOG_DATA) - public data: { - owners: ApplicationOwnerDto[]; - fileId: string; - submissionUuid: string; - } - ) { - this.fileId = data.fileId; - this.submissionUuid = data.submissionUuid; - this.owners = data.owners; - } - - async onUpdated() { - const updatedOwners = await this.applicationOwnerService.fetchBySubmissionId(this.submissionUuid); - if (updatedOwners) { - this.owners = updatedOwners.filter( - (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) - ); - this.isDirty = true; - } - } -} diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html index 0cdffa38c9..91d4429399 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html @@ -257,6 +257,8 @@
Owner Information
[owners]="parcel.owners" (onOwnersUpdated)="onOwnersUpdated.emit()" (onOwnerRemoved)="onOwnerRemoved($event)" + [documentService]="applicationDocumentService" + [ownerService]="applicationOwnerService" [disabled]="_disabled" >
@@ -339,6 +341,8 @@
Owner Information
[owners]="parcel.owners" (onOwnersUpdated)="onOwnersUpdated.emit()" (onOwnerRemoved)="onOwnerRemoved($event)" + [ownerService]="applicationOwnerService" + [documentService]="applicationDocumentService" [disabled]="_disabled" [isCrown]="true" > diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts index 072389f93b..1aef5b9bbe 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts @@ -18,9 +18,9 @@ import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; -import { ApplicationCrownOwnerDialogComponent } from '../application-crown-owner-dialog/application-crown-owner-dialog.component'; -import { ApplicationOwnerDialogComponent } from '../application-owner-dialog/application-owner-dialog.component'; -import { ApplicationOwnersDialogComponent } from '../application-owners-dialog/application-owners-dialog.component'; +import { CrownOwnerDialogComponent } from '../../../../../shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component'; +import { OwnerDialogComponent } from '../../../../../shared/owner-dialogs/owner-dialog/owner-dialog.component'; +import { AllOwnersDialogComponent } from '../../../../../shared/owner-dialogs/owners-dialog/all-owners-dialog.component'; import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; export interface ParcelEntryFormData { @@ -110,8 +110,8 @@ export class ParcelEntryComponent implements OnInit { constructor( private parcelService: ParcelService, private applicationParcelService: ApplicationParcelService, - private applicationOwnerService: ApplicationOwnerService, - private applicationDocumentService: ApplicationDocumentService, + public applicationOwnerService: ApplicationOwnerService, + public applicationDocumentService: ApplicationDocumentService, private dialog: MatDialog ) {} @@ -284,11 +284,13 @@ export class ParcelEntryComponent implements OnInit { } onAddNewOwner() { - const dialog = this.dialog.open(ApplicationOwnerDialogComponent, { + const dialog = this.dialog.open(OwnerDialogComponent, { data: { fileId: this.fileId, submissionUuid: this.submissionUuid, parcelUuid: this.parcel.uuid, + ownerService: this.applicationOwnerService, + documentService: this.applicationDocumentService, }, }); dialog.beforeClosed().subscribe((createdDto) => { @@ -301,11 +303,12 @@ export class ParcelEntryComponent implements OnInit { } onAddNewGovernmentContact() { - const dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { + const dialog = this.dialog.open(CrownOwnerDialogComponent, { data: { fileId: this.fileId, submissionUuid: this.submissionUuid, parcelUuid: this.parcel.uuid, + ownerService: this.applicationOwnerService, }, }); dialog.beforeClosed().subscribe((createdDto) => { @@ -345,7 +348,17 @@ export class ParcelEntryComponent implements OnInit { isSelected, }; }) - .sort(this.applicationOwnerService.sortOwners); + .sort(this.sortOwners); + } + + sortOwners(a: ApplicationOwnerDto, b: ApplicationOwnerDto) { + if (a.displayName < b.displayName) { + return -1; + } + if (a.displayName > b.displayName) { + return 1; + } + return 0; } onTypeOwner($event: Event) { @@ -357,11 +370,13 @@ export class ParcelEntryComponent implements OnInit { onSeeAllOwners() { this.dialog - .open(ApplicationOwnersDialogComponent, { + .open(AllOwnersDialogComponent, { data: { owners: this.owners, fileId: this.fileId, submissionUuid: this.submissionUuid, + ownerService: this.applicationOwnerService, + documentService: this.applicationDocumentService, }, }) .beforeClosed() diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html deleted file mode 100644 index 51ea96511d..0000000000 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html +++ /dev/null @@ -1,73 +0,0 @@ -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Type{{ element.type.label }}{{ element.position }}Full Name{{ element.displayName }}Organization NameOrganization Name / Ministry / Department - - {{ element.organizationName }} - Ministry/ Department{{ element.organizationName }}Phone{{ element.phoneNumber | mask : '(000) 000-0000' }}Email{{ element.email }}Actions - - - -
No owner information
-
diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts deleted file mode 100644 index e0f04f417f..0000000000 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; -import { ApplicationCrownOwnerDialogComponent } from '../application-crown-owner-dialog/application-crown-owner-dialog.component'; -import { ApplicationOwnerDialogComponent } from '../application-owner-dialog/application-owner-dialog.component'; - -@Component({ - selector: 'app-parcel-owners[owners][fileId][submissionUuid]', - templateUrl: './parcel-owners.component.html', - styleUrls: ['./parcel-owners.component.scss'], -}) -export class ParcelOwnersComponent { - @Output() onOwnersUpdated = new EventEmitter(); - @Output() onOwnerRemoved = new EventEmitter(); - - @Input() - public set owners(owners: ApplicationOwnerDto[]) { - this._owners = owners.sort(this.appOwnerService.sortOwners); - } - - @Input() - public set disabled(disabled: boolean) { - this._disabled = disabled; - } - - @Input() submissionUuid!: string; - @Input() fileId!: string; - @Input() parcelUuid?: string | undefined; - @Input() isCrown = false; - @Input() isDraft = false; - @Input() isShowAllOwners = false; - - _owners: ApplicationOwnerDto[] = []; - _disabled = false; - displayedColumns = ['type', 'position', 'displayName', 'organizationName', 'phone', 'email', 'actions']; - - constructor( - private dialog: MatDialog, - private appOwnerService: ApplicationOwnerService, - private confDialogService: ConfirmationDialogService - ) {} - - onEdit(owner: ApplicationOwnerDto) { - let dialog; - if (owner.type.code === OWNER_TYPE.CROWN) { - dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { - data: { - isDraft: this.isDraft, - parcelUuid: this.parcelUuid, - existingOwner: owner, - submissionUuid: this.submissionUuid, - }, - }); - } else { - dialog = this.dialog.open(ApplicationOwnerDialogComponent, { - data: { - isDraft: this.isDraft, - fileId: this.fileId, - submissionUuid: this.submissionUuid, - parcelUuid: this.parcelUuid, - existingOwner: owner, - }, - }); - } - dialog.beforeClosed().subscribe((updatedUuid) => { - if (updatedUuid) { - this.onOwnersUpdated.emit(); - } - }); - } - - async onRemove(uuid: string) { - this.onOwnerRemoved.emit(uuid); - } - - async onDelete(owner: ApplicationOwnerDto) { - this.confDialogService - .openDialog({ - body: `This action will remove ${owner.displayName} and its usage from the entire application. Are you sure you want to remove this owner? `, - }) - .subscribe(async (didConfirm) => { - if (didConfirm) { - await this.appOwnerService.delete(owner.uuid); - this.onOwnersUpdated.emit(); - } - }); - } -} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts index 744f8fb3ee..57f9693ad8 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts @@ -13,7 +13,6 @@ import { DeleteParcelDialogComponent } from './parcels/delete-parcel/delete-parc import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; -import { ParcelOwnersComponent } from './parcels/parcel-owners/parcel-owners.component'; @NgModule({ declarations: [ @@ -22,7 +21,6 @@ import { ParcelOwnersComponent } from './parcels/parcel-owners/parcel-owners.com ParcelEntryComponent, ParcelEntryConfirmationDialogComponent, DeleteParcelDialogComponent, - ParcelOwnersComponent, ], imports: [ CommonModule, @@ -42,7 +40,6 @@ import { ParcelOwnersComponent } from './parcels/parcel-owners/parcel-owners.com ParcelEntryComponent, ParcelEntryConfirmationDialogComponent, DeleteParcelDialogComponent, - ParcelOwnersComponent, ], }) export class EditSubmissionBaseModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts index d0352c217a..596788e2a6 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts @@ -2,10 +2,12 @@ import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { BehaviorSubject, takeUntil } from 'rxjs'; -import { ApplicationParcelUpdateDto } from '../../../../services/application-parcel/application-parcel.dto'; import { NoticeOfIntentOwnerDto } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; import { NoticeOfIntentOwnerService } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; -import { NoticeOfIntentParcelDto } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { + NoticeOfIntentParcelDto, + NoticeOfIntentParcelUpdateDto, +} from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; import { ToastService } from '../../../../services/toast/toast.service'; import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; @@ -106,13 +108,13 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft parcel.crownLandOwnerType = formData.crownLandOwnerType !== undefined ? formData.crownLandOwnerType : parcel.crownLandOwnerType; if (formData.owners) { - //parcel.owners = formData.owners; + parcel.owners = formData.owners; } } private async saveProgress() { if (this.isDirty || this.newParcelAdded) { - const parcelsToUpdate: ApplicationParcelUpdateDto[] = []; + const parcelsToUpdate: NoticeOfIntentParcelUpdateDto[] = []; for (const parcel of this.parcels) { parcelsToUpdate.push({ uuid: parcel.uuid, @@ -126,7 +128,7 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft ownershipTypeCode: parcel.ownershipTypeCode, isConfirmedByApplicant: parcel.isConfirmedByApplicant, crownLandOwnerType: parcel.crownLandOwnerType, - ownerUuids: [], + ownerUuids: parcel.owners.map((owner) => owner.uuid), }); } await this.noiParcelService.update(parcelsToUpdate); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html index ad624d34bd..9c3365cc68 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html @@ -2,8 +2,8 @@
- The answer to the following question will change the rest of the application form. Do not change this answer once - selected. + The answer to the following question will change the rest of the notice of intent form. Do not change this answer + once selected.
Owner Information [owners]="parcel.owners" (onOwnersUpdated)="onOwnersUpdated.emit()" (onOwnerRemoved)="onOwnerRemoved($event)" + [ownerService]="noticeOfIntentOwnerService" + [documentService]="noticeOfIntentDocumentService" [disabled]="_disabled" >
@@ -332,16 +334,16 @@
Owner Information
Add new government contact
- - - - - - - - - - + + + + + + + + + +
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts index bb0f985ece..8d7c068727 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts @@ -11,14 +11,13 @@ import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-in import { NoticeOfIntentParcelDto } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; import { ParcelService } from '../../../../../services/parcel/parcel.service'; -import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; +import { CrownOwnerDialogComponent } from '../../../../../shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component'; +import { OwnerDialogComponent } from '../../../../../shared/owner-dialogs/owner-dialog/owner-dialog.component'; +import { AllOwnersDialogComponent } from '../../../../../shared/owner-dialogs/owners-dialog/all-owners-dialog.component'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { RemoveFileConfirmationDialogComponent } from '../../../../applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; -import { ApplicationCrownOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component'; -import { ApplicationOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component'; -import { ApplicationOwnersDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component'; import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; export interface ParcelEntryFormData { @@ -101,15 +100,14 @@ export class ParcelEntryComponent implements OnInit { ownerInput = new FormControl(null); - DOCUMENT_TYPES = DOCUMENT_TYPE; PARCEL_OWNERSHIP_TYPES = PARCEL_OWNERSHIP_TYPE; maxPurchasedDate = new Date(); constructor( private parcelService: ParcelService, private noticeOfIntentParcelService: NoticeOfIntentParcelService, - private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, - private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + public noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + public noticeOfIntentDocumentService: NoticeOfIntentDocumentService, private dialog: MatDialog ) {} @@ -282,11 +280,13 @@ export class ParcelEntryComponent implements OnInit { } onAddNewOwner() { - const dialog = this.dialog.open(ApplicationOwnerDialogComponent, { + const dialog = this.dialog.open(OwnerDialogComponent, { data: { fileId: this.fileId, submissionUuid: this.submissionUuid, parcelUuid: this.parcel.uuid, + ownerService: this.noticeOfIntentOwnerService, + documentService: this.noticeOfIntentDocumentService, }, }); dialog.beforeClosed().subscribe((createdDto) => { @@ -299,11 +299,12 @@ export class ParcelEntryComponent implements OnInit { } onAddNewGovernmentContact() { - const dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { + const dialog = this.dialog.open(CrownOwnerDialogComponent, { data: { fileId: this.fileId, submissionUuid: this.submissionUuid, parcelUuid: this.parcel.uuid, + ownerService: this.noticeOfIntentOwnerService, }, }); dialog.beforeClosed().subscribe((createdDto) => { @@ -355,11 +356,13 @@ export class ParcelEntryComponent implements OnInit { onSeeAllOwners() { this.dialog - .open(ApplicationOwnersDialogComponent, { + .open(AllOwnersDialogComponent, { data: { owners: this.owners, fileId: this.fileId, submissionUuid: this.submissionUuid, + ownerService: this.noticeOfIntentOwnerService, + documentService: this.noticeOfIntentDocumentService, }, }) .beforeClosed() diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss deleted file mode 100644 index fb105e09eb..0000000000 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -@use '../../../../../../styles/functions' as *; -@use '../../../../../../styles/colors'; - -.actions { - button:not(:last-child) { - margin-right: rem(8) !important; - } -} - -.no-data-text { - text-align: center; - color: colors.$grey; - padding-top: rem(12); -} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts deleted file mode 100644 index b36089cde8..0000000000 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatDialog } from '@angular/material/dialog'; -import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; -import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; - -import { ParcelOwnersComponent } from './parcel-owners.component'; - -describe('ParcelOwnersComponent', () => { - let component: ParcelOwnersComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - providers: [ - { - provide: MatDialog, - useValue: {}, - }, - { - provide: NoticeOfIntentOwnerService, - useValue: {}, - }, - { - provide: ConfirmationDialogService, - useValue: {}, - }, - ], - declarations: [ParcelOwnersComponent], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(ParcelOwnersComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts deleted file mode 100644 index ebfcd50c62..0000000000 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { NoticeOfIntentOwnerDto } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; -import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; -import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; -import { ApplicationCrownOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component'; -import { ApplicationOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component'; - -@Component({ - selector: 'app-parcel-owners[owners][fileId][submissionUuid]', - templateUrl: './parcel-owners.component.html', - styleUrls: ['./parcel-owners.component.scss'], -}) -export class ParcelOwnersComponent { - @Output() onOwnersUpdated = new EventEmitter(); - @Output() onOwnerRemoved = new EventEmitter(); - - @Input() - public set owners(owners: NoticeOfIntentOwnerDto[]) { - this._owners = owners.sort(this.noticeOfIntentOwnerService.sortOwners); - } - - @Input() - public set disabled(disabled: boolean) { - this._disabled = disabled; - } - - @Input() submissionUuid!: string; - @Input() fileId!: string; - @Input() parcelUuid?: string | undefined; - @Input() isCrown = false; - @Input() isDraft = false; - @Input() isShowAllOwners = false; - - _owners: NoticeOfIntentOwnerDto[] = []; - _disabled = false; - displayedColumns = ['type', 'position', 'displayName', 'organizationName', 'phone', 'email', 'actions']; - - constructor( - private dialog: MatDialog, - private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, - private confDialogService: ConfirmationDialogService - ) {} - - onEdit(owner: NoticeOfIntentOwnerDto) { - let dialog; - if (owner.type.code === OWNER_TYPE.CROWN) { - dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { - data: { - isDraft: this.isDraft, - parcelUuid: this.parcelUuid, - existingOwner: owner, - submissionUuid: this.submissionUuid, - }, - }); - } else { - dialog = this.dialog.open(ApplicationOwnerDialogComponent, { - data: { - isDraft: this.isDraft, - fileId: this.fileId, - submissionUuid: this.submissionUuid, - parcelUuid: this.parcelUuid, - existingOwner: owner, - }, - }); - } - dialog.beforeClosed().subscribe((updatedUuid) => { - if (updatedUuid) { - this.onOwnersUpdated.emit(); - } - }); - } - - async onRemove(uuid: string) { - this.onOwnerRemoved.emit(uuid); - } - - async onDelete(owner: NoticeOfIntentOwnerDto) { - this.confDialogService - .openDialog({ - body: `This action will remove ${owner.displayName} and its usage from the entire application. Are you sure you want to remove this owner? `, - }) - .subscribe(async (didConfirm) => { - if (didConfirm) { - await this.noticeOfIntentOwnerService.delete(owner.uuid); - this.onOwnersUpdated.emit(); - } - }); - } -} diff --git a/portal-frontend/src/app/services/application-owner/application-owner.service.ts b/portal-frontend/src/app/services/application-owner/application-owner.service.ts index 290e3a83db..ba3684a4eb 100644 --- a/portal-frontend/src/app/services/application-owner/application-owner.service.ts +++ b/portal-frontend/src/app/services/application-owner/application-owner.service.ts @@ -116,16 +116,6 @@ export class ApplicationOwnerService { return undefined; } - sortOwners(a: ApplicationOwnerDto, b: ApplicationOwnerDto) { - if (a.displayName < b.displayName) { - return -1; - } - if (a.displayName > b.displayName) { - return 1; - } - return 0; - } - async uploadCorporateSummary(applicationFileId: string, file: File) { try { return await this.documentService.uploadFile<{ uuid: string }>( diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html rename to portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.html diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.scss similarity index 72% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss rename to portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.scss index a4fd5b9b0f..f9fa4857b8 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss +++ b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../../styles/functions' as *; -@use '../../../../../../styles/colors'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; .actions { button:not(:last-child) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.spec.ts similarity index 67% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts rename to portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.spec.ts index 97ab2104aa..c51f7e8bb3 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.spec.ts @@ -2,13 +2,13 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; -import { ApplicationCrownOwnerDialogComponent } from './application-crown-owner-dialog.component'; +import { CrownOwnerDialogComponent } from './crown-owner-dialog.component'; describe('ApplicationCrownOwnerDialogComponent', () => { - let component: ApplicationCrownOwnerDialogComponent; - let fixture: ComponentFixture; + let component: CrownOwnerDialogComponent; + let fixture: ComponentFixture; let mockAppOwnerService: DeepMocked; beforeEach(async () => { @@ -29,11 +29,11 @@ describe('ApplicationCrownOwnerDialogComponent', () => { useValue: {}, }, ], - declarations: [ApplicationCrownOwnerDialogComponent], + declarations: [CrownOwnerDialogComponent], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - fixture = TestBed.createComponent(ApplicationCrownOwnerDialogComponent); + fixture = TestBed.createComponent(CrownOwnerDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.ts similarity index 64% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts rename to portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.ts index 8aba0af222..4d4e80f488 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.ts @@ -5,16 +5,22 @@ import { ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, -} from '../../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; +} from '../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { + NoticeOfIntentOwnerCreateDto, + NoticeOfIntentOwnerDto, + NoticeOfIntentOwnerUpdateDto, +} from '../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { OWNER_TYPE } from '../../dto/owner.dto'; @Component({ - selector: 'app-application-crown-owner-dialog', - templateUrl: './application-crown-owner-dialog.component.html', - styleUrls: ['./application-crown-owner-dialog.component.scss'], + selector: 'app-crown-owner-dialog', + templateUrl: './crown-owner-dialog.component.html', + styleUrls: ['./crown-owner-dialog.component.scss'], }) -export class ApplicationCrownOwnerDialogComponent { +export class CrownOwnerDialogComponent { ministryName = new FormControl('', [Validators.required]); firstName = new FormControl('', [Validators.required]); lastName = new FormControl('', [Validators.required]); @@ -33,14 +39,14 @@ export class ApplicationCrownOwnerDialogComponent { }); constructor( - private dialogRef: MatDialogRef, - private appOwnerService: ApplicationOwnerService, + private dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: { submissionUuid: string; fileId: string; parcelUuid?: string; - existingOwner?: ApplicationOwnerDto; + ownerService: ApplicationOwnerService | NoticeOfIntentOwnerService; + existingOwner?: ApplicationOwnerDto | NoticeOfIntentOwnerDto; } ) { if (data && data.existingOwner) { @@ -55,7 +61,7 @@ export class ApplicationCrownOwnerDialogComponent { } async onCreate() { - const createDto: ApplicationOwnerCreateDto = { + const createDto: ApplicationOwnerCreateDto & NoticeOfIntentOwnerCreateDto = { organizationName: this.ministryName.getRawValue() || undefined, firstName: this.firstName.getRawValue() || undefined, lastName: this.lastName.getRawValue() || undefined, @@ -63,9 +69,10 @@ export class ApplicationCrownOwnerDialogComponent { phoneNumber: this.phoneNumber.getRawValue()!, typeCode: OWNER_TYPE.CROWN, applicationSubmissionUuid: this.data.submissionUuid, + noticeOfIntentSubmissionUuid: this.data.submissionUuid, }; - const res = await this.appOwnerService.create(createDto); + const res = await this.data.ownerService.create(createDto); this.dialogRef.close(res); } @@ -74,7 +81,7 @@ export class ApplicationCrownOwnerDialogComponent { } async onSave() { - const updateDto: ApplicationOwnerUpdateDto = { + const updateDto: ApplicationOwnerUpdateDto & NoticeOfIntentOwnerUpdateDto = { organizationName: this.ministryName.getRawValue(), firstName: this.firstName.getRawValue(), lastName: this.lastName.getRawValue(), @@ -83,7 +90,7 @@ export class ApplicationCrownOwnerDialogComponent { typeCode: OWNER_TYPE.CROWN, }; if (this.existingUuid) { - const res = await this.appOwnerService.update(this.existingUuid, updateDto); + const res = await this.data.ownerService.update(this.existingUuid, updateDto); this.dialogRef.close(res); } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html rename to portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.html diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.scss similarity index 75% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss rename to portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.scss index 2af88008a7..a31d29d504 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss +++ b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../../styles/functions' as *; -@use '../../../../../../styles/colors'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; .actions { button:not(:last-child) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.spec.ts similarity index 69% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts rename to portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.spec.ts index 8e9cd1894e..0c3aad1e35 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.spec.ts @@ -2,15 +2,15 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { CodeService } from '../../../../../services/code/code.service'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { CodeService } from '../../../services/code/code.service'; -import { ApplicationOwnerDialogComponent } from './application-owner-dialog.component'; +import { OwnerDialogComponent } from './owner-dialog.component'; describe('ApplicationOwnerDialogComponent', () => { - let component: ApplicationOwnerDialogComponent; - let fixture: ComponentFixture; + let component: OwnerDialogComponent; + let fixture: ComponentFixture; let mockAppOwnerService: DeepMocked; let mockCodeService: DeepMocked; let mockAppDocService: DeepMocked; @@ -47,11 +47,11 @@ describe('ApplicationOwnerDialogComponent', () => { useValue: {}, }, ], - declarations: [ApplicationOwnerDialogComponent], + declarations: [OwnerDialogComponent], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - fixture = TestBed.createComponent(ApplicationOwnerDialogComponent); + fixture = TestBed.createComponent(OwnerDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.ts similarity index 76% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts rename to portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.ts index 0826020249..cfd2303c8e 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.ts @@ -2,26 +2,29 @@ import { Component, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; import { ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, -} from '../../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { CodeService } from '../../../../../services/code/code.service'; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../../shared/dto/document.dto'; -import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; -import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; -import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; +} from '../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { CodeService } from '../../../services/code/code.service'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerCreateDto } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../dto/document.dto'; +import { OWNER_TYPE } from '../../dto/owner.dto'; +import { FileHandle } from '../../file-drag-drop/drag-drop.directive'; +import { RemoveFileConfirmationDialogComponent } from '../../../features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; @Component({ - selector: 'app-application-owner-dialog', - templateUrl: './application-owner-dialog.component.html', - styleUrls: ['./application-owner-dialog.component.scss'], + selector: 'app-owner-dialog', + templateUrl: './owner-dialog.component.html', + styleUrls: ['./owner-dialog.component.scss'], }) -export class ApplicationOwnerDialogComponent { +export class OwnerDialogComponent { OWNER_TYPE = OWNER_TYPE; type = new FormControl(OWNER_TYPE.INDIVIDUAL); firstName = new FormControl('', [Validators.required]); @@ -48,10 +51,8 @@ export class ApplicationOwnerDialogComponent { private documentCodes: DocumentTypeDto[] = []; constructor( - private dialogRef: MatDialogRef, - private appOwnerService: ApplicationOwnerService, + private dialogRef: MatDialogRef, private codeService: CodeService, - private documentService: ApplicationDocumentService, private dialog: MatDialog, @Inject(MAT_DIALOG_DATA) public data: { @@ -60,6 +61,8 @@ export class ApplicationOwnerDialogComponent { isDraft: boolean; parcelUuid?: string; existingOwner?: ApplicationOwnerDto; + documentService: ApplicationDocumentService | NoticeOfIntentDocumentService; + ownerService: ApplicationOwnerService | NoticeOfIntentOwnerService; } ) { if (data && data.existingOwner) { @@ -101,7 +104,7 @@ export class ApplicationOwnerDialogComponent { } const documentUuid = await this.uploadPendingFile(this.pendingFile); - const createDto: ApplicationOwnerCreateDto = { + const createDto: ApplicationOwnerCreateDto & NoticeOfIntentOwnerCreateDto = { organizationName: this.organizationName.getRawValue() || undefined, firstName: this.firstName.getRawValue() || undefined, lastName: this.lastName.getRawValue() || undefined, @@ -110,9 +113,10 @@ export class ApplicationOwnerDialogComponent { phoneNumber: this.phoneNumber.getRawValue()!, typeCode: this.type.getRawValue()!, applicationSubmissionUuid: this.data.submissionUuid, + noticeOfIntentSubmissionUuid: this.data.submissionUuid, }; - const res = await this.appOwnerService.create(createDto); + const res = await this.data.ownerService.create(createDto); this.dialogRef.close(res); } @@ -132,7 +136,7 @@ export class ApplicationOwnerDialogComponent { typeCode: this.type.getRawValue()!, }; if (this.existingUuid) { - const res = await this.appOwnerService.update(this.existingUuid, updateDto); + const res = await this.data.ownerService.update(this.existingUuid, updateDto); this.dialogRef.close(res); } } @@ -186,7 +190,7 @@ export class ApplicationOwnerDialogComponent { const fileURL = URL.createObjectURL(this.pendingFile); window.open(fileURL, '_blank'); } else if (this.existingUuid && this.data.existingOwner?.corporateSummary?.uuid) { - const res = await this.documentService.openFile(this.data.existingOwner?.corporateSummary?.uuid); + const res = await this.data.documentService.openFile(this.data.existingOwner?.corporateSummary?.uuid); if (res) { window.open(res.url, '_blank'); } @@ -196,7 +200,7 @@ export class ApplicationOwnerDialogComponent { private async uploadPendingFile(file?: File) { let documentUuid; if (file) { - documentUuid = await this.appOwnerService.uploadCorporateSummary(this.data.fileId, file); + documentUuid = await this.data.ownerService.uploadCorporateSummary(this.data.fileId, file); if (!documentUuid) { return; } diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.html similarity index 82% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html rename to portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.html index 209aac8011..261a10027b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html +++ b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.html @@ -9,6 +9,8 @@

All Owners

[owners]="owners" [fileId]="fileId" [isShowAllOwners]="true" + [documentService]="data.documentService" + [ownerService]="data.ownerService" >
diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.scss similarity index 100% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss rename to portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.scss diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.spec.ts similarity index 66% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts rename to portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.spec.ts index 002133241d..3dc13aead7 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.spec.ts @@ -2,12 +2,12 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { ApplicationOwnersDialogComponent } from './application-owners-dialog.component'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { AllOwnersDialogComponent } from './all-owners-dialog.component'; describe('ApplicationOwnersDialogComponent', () => { - let component: ApplicationOwnersDialogComponent; - let fixture: ComponentFixture; + let component: AllOwnersDialogComponent; + let fixture: ComponentFixture; let mockAppOwnerService: DeepMocked; beforeEach(async () => { @@ -24,11 +24,11 @@ describe('ApplicationOwnersDialogComponent', () => { useValue: {}, }, ], - declarations: [ApplicationOwnersDialogComponent], + declarations: [AllOwnersDialogComponent], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - fixture = TestBed.createComponent(ApplicationOwnersDialogComponent); + fixture = TestBed.createComponent(AllOwnersDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.ts b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.ts new file mode 100644 index 0000000000..445d22530c --- /dev/null +++ b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.ts @@ -0,0 +1,47 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationOwnerDto } from '../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { OWNER_TYPE } from '../../dto/owner.dto'; + +@Component({ + selector: 'app-all-owners-dialog', + templateUrl: './all-owners-dialog.component.html', + styleUrls: ['./all-owners-dialog.component.scss'], +}) +export class AllOwnersDialogComponent { + isDirty = false; + owners: ApplicationOwnerDto[] | NoticeOfIntentOwnerDto[] = []; + fileId: string; + submissionUuid: string; + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: { + owners: ApplicationOwnerDto[] | NoticeOfIntentOwnerDto[]; + fileId: string; + submissionUuid: string; + ownerService: ApplicationOwnerService | NoticeOfIntentOwnerService; + documentService: ApplicationDocumentService | NoticeOfIntentDocumentService; + } + ) { + this.fileId = data.fileId; + this.submissionUuid = data.submissionUuid; + this.owners = data.owners; + } + + async onUpdated() { + const updatedOwners = await this.data.ownerService.fetchBySubmissionId(this.submissionUuid); + if (updatedOwners) { + // @ts-ignore Bug with Typescript https://github.com/microsoft/TypeScript/issues/44373 + this.owners = updatedOwners.filter( + (owner: { type: { code: OWNER_TYPE } }) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) + ); + this.isDirty = true; + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.html similarity index 97% rename from portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html rename to portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.html index 51ea96511d..0041cf444a 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html +++ b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.html @@ -1,5 +1,5 @@
- +
diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.scss similarity index 66% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss rename to portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.scss index fb105e09eb..bea80e08ec 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss +++ b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../../styles/functions' as *; -@use '../../../../../../styles/colors'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; .actions { button:not(:last-child) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.spec.ts similarity index 82% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts rename to portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.spec.ts index 36430e28e6..1707807c8b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.spec.ts @@ -1,8 +1,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { ConfirmationDialogService } from '../../confirmation-dialog/confirmation-dialog.service'; import { ParcelOwnersComponent } from './parcel-owners.component'; diff --git a/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.ts new file mode 100644 index 0000000000..2d99ada35f --- /dev/null +++ b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.ts @@ -0,0 +1,110 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTableDataSource } from '@angular/material/table'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationOwnerDto } from '../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { ConfirmationDialogService } from '../../confirmation-dialog/confirmation-dialog.service'; +import { OWNER_TYPE } from '../../dto/owner.dto'; +import { CrownOwnerDialogComponent } from '../crown-owner-dialog/crown-owner-dialog.component'; +import { OwnerDialogComponent } from '../owner-dialog/owner-dialog.component'; + +@Component({ + selector: 'app-parcel-owners[owners][fileId][submissionUuid][ownerService]', + templateUrl: './parcel-owners.component.html', + styleUrls: ['./parcel-owners.component.scss'], +}) +export class ParcelOwnersComponent { + @Output() onOwnersUpdated = new EventEmitter(); + @Output() onOwnerRemoved = new EventEmitter(); + + @Input() ownerService!: ApplicationOwnerService | NoticeOfIntentOwnerService; + @Input() documentService!: ApplicationDocumentService | NoticeOfIntentDocumentService; + dataSource = new MatTableDataSource([]); + + @Input() + public set owners(owners: ApplicationOwnerDto[] | NoticeOfIntentOwnerDto[]) { + const sorted = owners.sort(this.sortOwners); + // @ts-ignore + this.dataSource = new MatTableDataSource(sorted); + } + + sortOwners(a: ApplicationOwnerDto | NoticeOfIntentOwnerDto, b: ApplicationOwnerDto | NoticeOfIntentOwnerDto) { + if (a.displayName < b.displayName) { + return -1; + } + if (a.displayName > b.displayName) { + return 1; + } + return 0; + } + + @Input() + public set disabled(disabled: boolean) { + this._disabled = disabled; + } + + @Input() submissionUuid!: string; + @Input() fileId!: string; + @Input() parcelUuid?: string | undefined; + @Input() isCrown = false; + @Input() isDraft = false; + @Input() isShowAllOwners = false; + + _disabled = false; + displayedColumns = ['type', 'position', 'displayName', 'organizationName', 'phone', 'email', 'actions']; + + constructor(private dialog: MatDialog, private confDialogService: ConfirmationDialogService) {} + + onEdit(owner: ApplicationOwnerDto) { + let dialog; + if (owner.type.code === OWNER_TYPE.CROWN) { + dialog = this.dialog.open(CrownOwnerDialogComponent, { + data: { + isDraft: this.isDraft, + parcelUuid: this.parcelUuid, + existingOwner: owner, + submissionUuid: this.submissionUuid, + ownerService: this.ownerService, + }, + }); + } else { + dialog = this.dialog.open(OwnerDialogComponent, { + data: { + isDraft: this.isDraft, + fileId: this.fileId, + submissionUuid: this.submissionUuid, + parcelUuid: this.parcelUuid, + existingOwner: owner, + ownerService: this.ownerService, + documentService: this.documentService, + }, + }); + } + dialog.beforeClosed().subscribe((updatedUuid) => { + if (updatedUuid) { + this.onOwnersUpdated.emit(); + } + }); + } + + async onRemove(uuid: string) { + this.onOwnerRemoved.emit(uuid); + } + + async onDelete(owner: ApplicationOwnerDto | NoticeOfIntentOwnerDto) { + this.confDialogService + .openDialog({ + body: `This action will remove ${owner.displayName} and its usage from the entire application. Are you sure you want to remove this owner? `, + }) + .subscribe(async (didConfirm) => { + if (didConfirm) { + await this.ownerService.delete(owner.uuid); + this.onOwnersUpdated.emit(); + } + }); + } +} diff --git a/portal-frontend/src/app/shared/shared.module.ts b/portal-frontend/src/app/shared/shared.module.ts index 3d55310071..75b24121ec 100644 --- a/portal-frontend/src/app/shared/shared.module.ts +++ b/portal-frontend/src/app/shared/shared.module.ts @@ -31,6 +31,10 @@ import { DragDropDirective } from './file-drag-drop/drag-drop.directive'; import { FileDragDropComponent } from './file-drag-drop/file-drag-drop.component'; import { InfoBannerComponent } from './info-banner/info-banner.component'; import { NoDataComponent } from './no-data/no-data.component'; +import { CrownOwnerDialogComponent } from './owner-dialogs/crown-owner-dialog/crown-owner-dialog.component'; +import { OwnerDialogComponent } from './owner-dialogs/owner-dialog/owner-dialog.component'; +import { AllOwnersDialogComponent } from './owner-dialogs/owners-dialog/all-owners-dialog.component'; +import { ParcelOwnersComponent } from './owner-dialogs/parcel-owners/parcel-owners.component'; import { EmailValidPipe } from './pipes/emailValid.pipe'; import { FileSizePipe } from './pipes/fileSize.pipe'; import { MomentPipe } from './pipes/moment.pipe'; @@ -59,6 +63,15 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen NgxMaskDirective, NgxMaskPipe, MatRadioModule, + MatDialogModule, + MatTableModule, + FormsModule, + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonToggleModule, + MatCheckboxModule, + MatIconModule, ], declarations: [ FileDragDropComponent, @@ -74,6 +87,10 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen CustomStepperComponent, MomentPipe, PresribedBodyComponent, + OwnerDialogComponent, + CrownOwnerDialogComponent, + AllOwnersDialogComponent, + ParcelOwnersComponent, ], exports: [ CommonModule, @@ -116,6 +133,10 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen NgxMaskPipe, MomentPipe, PresribedBodyComponent, + OwnerDialogComponent, + CrownOwnerDialogComponent, + AllOwnersDialogComponent, + ParcelOwnersComponent, ], }) export class SharedModule { diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts index 6c2e94ef6a..f577977c4a 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts @@ -7,6 +7,8 @@ import { NoticeOfIntentSubmissionToSubmissionStatusDto, } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.entity'; +import { NoticeOfIntentOwnerDto } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; import { NoticeOfIntentSubmissionDetailedDto, NoticeOfIntentSubmissionDto, @@ -43,6 +45,20 @@ export class NoticeOfIntentSubmissionProfile extends AutomapperProfile { return ad.status.statusType; }), ), + forMember( + (a) => a.owners, + mapFrom((ad) => { + if (ad.owners) { + return this.mapper.mapArray( + ad.owners, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + } else { + return []; + } + }), + ), ); createMap( diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts index 9cd7278d9d..ccb29748bd 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -1,14 +1,14 @@ import { AutoMap } from '@automapper/classes'; import { - IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, MaxLength, } from 'class-validator'; -import { ApplicationStatusDto } from '../../alcs/application/application-submission-status/submission-status.dto'; import { NoticeOfIntentStatusDto } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { ApplicationOwnerDto } from '../application-submission/application-owner/application-owner.dto'; +import { NoticeOfIntentOwnerDto } from './notice-of-intent-owner/notice-of-intent-owner.dto'; export const MAX_DESCRIPTION_FIELD_LENGTH = 4000; @@ -35,6 +35,7 @@ export class NoticeOfIntentSubmissionDto { type: string; status: NoticeOfIntentStatusDto; + owners: NoticeOfIntentOwnerDto[]; canEdit: boolean; canView: boolean; diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index db15158ca5..dc5a46ed87 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -34,7 +34,13 @@ export class NoticeOfIntentSubmissionService { private logger: Logger = new Logger(NoticeOfIntentSubmissionService.name); private DEFAULT_RELATIONS: FindOptionsRelations = { - //TODO + owners: { + type: true, + corporateSummary: { + document: true, + }, + parcels: true, + }, }; constructor( From 1595b0e2124d5cf6ac952521b274c54d415197eb Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 11:19:58 -0700 Subject: [PATCH 4/5] Add table descriptions --- .../1691777711797-add_noi_table_comments.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691777711797-add_noi_table_comments.ts diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691777711797-add_noi_table_comments.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691777711797-add_noi_table_comments.ts new file mode 100644 index 0000000000..9ac4860e6c --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691777711797-add_noi_table_comments.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNoiTableComments1691777711797 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + COMMENT ON TABLE "alcs"."notice_of_intent_parcel_ownership_type" IS 'Parcel Ownership types used for NOI Parcels'; + COMMENT ON TABLE "alcs"."notice_of_intent_parcel" IS 'Parcels that are linked to Notice of Intent Submissions'; + COMMENT ON TABLE "alcs"."notice_of_intent_owner" IS 'Owners for Notice of Intent Submissions'; + COMMENT ON TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" IS 'Join table that links Owners to Parcels'; + COMMENT ON TABLE "alcs"."notice_of_intent_submission_status_type" IS 'The code table for Notice of Intent Submissions Statuses'; + COMMENT ON TABLE "alcs"."notice_of_intent_submission_to_submission_status" IS 'Join table to link Notice of Intent Submissions to their Statuses'; + `, + ); + } + + public async down(): Promise { + //No + } +} From a7487e4bd2073cb93c9ec9050040bea452fb1e2d Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 13:04:18 -0700 Subject: [PATCH 5/5] Code Review Feedback --- .../parcel-entry/parcel-entry.component.html | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html index 9c3365cc68..70ccd10264 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html @@ -334,16 +334,18 @@
Owner Information
Add new government contact
- - - - - - - - - - +
Type {{ element.type.label }}