From ac4571f4a873fd677b439a85da7bb8ad9489486e Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 19 Jul 2023 14:25:16 -0700 Subject: [PATCH 1/2] Add Local Government options for LG Users * Replace owners with their own contact name * Create new Government type Owner * Update set primary contact to accept multiple types --- .../application-details.component.html | 9 +- .../application-details.component.ts | 11 +- .../other-parcels/other-parcels.component.ts | 4 +- .../application-owners-dialog.component.ts | 6 +- .../parcel-details.component.ts | 12 +- .../primary-contact.component.html | 58 ++++++--- .../primary-contact.component.spec.ts | 10 ++ .../primary-contact.component.ts | 115 ++++++++++++------ .../application-owner.dto.ts | 12 +- .../auth-interceptor.service.spec.ts | 2 +- .../authentication/authentication.dto.ts | 10 ++ .../authentication/authentication.service.ts | 12 +- .../authentication/token-refresh.service.ts | 2 +- .../shared/header/header.component.spec.ts | 2 +- .../src/app/shared/header/header.component.ts | 2 +- .../application-local-government.service.ts | 1 - services/apps/alcs/src/main.module.ts | 6 +- .../application-owner.controller.spec.ts | 8 +- .../application-owner.controller.ts | 41 ++++--- .../application-owner.dto.ts | 16 ++- .../application-owner.service.ts | 24 ++-- ...ation-submission-validator.service.spec.ts | 34 ++++++ ...pplication-submission-validator.service.ts | 10 +- ...1689806720586-add_government_owner_type.ts | 17 +++ .../alcs/src/user/user.controller.spec.ts | 35 ++++-- .../apps/alcs/src/user/user.controller.ts | 7 +- services/apps/alcs/src/user/user.dto.ts | 2 + services/apps/alcs/src/user/user.module.ts | 6 +- .../apps/alcs/src/user/user.service.spec.ts | 66 +++++++--- services/apps/alcs/src/user/user.service.ts | 16 ++- 30 files changed, 401 insertions(+), 155 deletions(-) create mode 100644 portal-frontend/src/app/services/authentication/authentication.dto.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1689806720586-add_government_owner_type.ts diff --git a/portal-frontend/src/app/features/application-details/application-details.component.html b/portal-frontend/src/app/features/application-details/application-details.component.html index 6dee41c5ba..5b108b7b3f 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/application-details/application-details.component.html @@ -36,8 +36,15 @@

3. Primary Contact

- Organization (optional) + Organization (optional) Ministry/Department Responsible + Department
{{ primaryContact?.organizationName }} diff --git a/portal-frontend/src/app/features/application-details/application-details.component.ts b/portal-frontend/src/app/features/application-details/application-details.component.ts index 4dc14c1e2d..071fe7ed2f 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.ts +++ b/portal-frontend/src/app/features/application-details/application-details.component.ts @@ -55,9 +55,14 @@ export class ApplicationDetailsComponent implements OnInit, OnDestroy { if (app) { this.primaryContact = app.owners.find((owner) => owner.uuid === app.primaryContactOwnerUuid); this.populateLocalGovernment(app.localGovernmentUuid); - this.needsAuthorizationLetter = !( - app.owners.length === 1 && app.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL - ); + + this.needsAuthorizationLetter = + !(this.primaryContact?.type.code === APPLICATION_OWNER.GOVERNMENT) && + !( + app.owners.length === 1 && + (app.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || + app.owners[0].type.code === APPLICATION_OWNER.GOVERNMENT) + ); } }); diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts b/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts index d710e033dd..b576c4b562 100644 --- a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts +++ b/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts @@ -70,7 +70,9 @@ export class OtherParcelsComponent extends StepComponent implements OnInit, OnDe this.application = application; this.fileId = application.fileNumber; this.submissionUuid = application.uuid; - const nonAgentOwners = application.owners.filter((owner) => owner.type.code !== APPLICATION_OWNER.AGENT); + const nonAgentOwners = application.owners.filter( + (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + ); this.owners = nonAgentOwners.map((o) => ({ ...o, parcels: o.parcels.filter((p) => p.parcelType === PARCEL_TYPE.OTHER), diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts b/portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts index c44bfa2a41..53ae86801d 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts +++ b/portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts @@ -1,6 +1,6 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { ApplicationOwnerDto, APPLICATION_OWNER } from '../../../../services/application-owner/application-owner.dto'; +import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; @Component({ @@ -31,7 +31,9 @@ export class ApplicationOwnersDialogComponent { async onUpdated() { const updatedOwners = await this.applicationOwnerService.fetchBySubmissionId(this.submissionUuid); if (updatedOwners) { - this.owners = updatedOwners.filter((owner) => owner.type.code !== APPLICATION_OWNER.AGENT); + this.owners = updatedOwners.filter( + (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + ); this.isDirty = true; } } diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts index db42a3ea8c..dcc2ee3388 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts +++ b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts @@ -48,10 +48,10 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft this.fileId = applicationSubmission.fileNumber; this.submissionUuid = applicationSubmission.uuid; this.loadParcels(); - const nonAgentOwners = applicationSubmission.owners.filter( - (owner) => owner.type.code !== APPLICATION_OWNER.AGENT + const parcelOwners = applicationSubmission.owners.filter( + (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) ); - this.$owners.next(nonAgentOwners); + this.$owners.next(parcelOwners); } }); @@ -159,8 +159,10 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft async onOwnersUpdated() { const owners = await this.applicationOwnerService.fetchBySubmissionId(this.submissionUuid); if (owners) { - const nonAgentOwners = owners.filter((owner) => owner.type.code !== APPLICATION_OWNER.AGENT); - this.$owners.next(nonAgentOwners); + const parcelOwners = owners.filter( + (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + ); + this.$owners.next(parcelOwners); } } diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html index 6f9528b20d..313af24e69 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html @@ -1,17 +1,21 @@

Primary Contact

-

Select from the listed parcel owners or identify a third party agent

+

Select from the listed parcel owners or identify a third party agent

+

+ Identify staff from the local or first nation government listed below or a third-party agent to act as the primary + contact. +

*All fields are required unless stated optional.

Documents needed for this step:
    -
  • Authorization Letter (if applicable)
  • +
  • Authorization Letters (if applicable)
- +
{{ owner.displayName }}
checkPrimary Contact
-
Third Party Agent on behalf of Owner(s)
+ +
{{governmentName}} Staff
+ +
checkPrimary Contact
+
+
Third-Party Agent
- + + @@ -109,19 +127,25 @@

Primary Contact Information

Primary Contact Authorization Letters

An authorization letter must be provided if: -
    -
  1. the parcel under application is owned by more than one person;
  2. -
  3. the parcel(s) is owned by an organization; or
  4. -
  5. the parcel(s) is owned by a corporation (private, Crown, local government, First Nations); or
  6. -
  7. the application is being submitted by a third-party agent on behalf of the land owner(s)
  8. -
-

- The authorization letter must be signed by all individual land owners and the majority of directors in - organization land owners listed in Step 1. Please consult the Supporting Documentation page of ALC website for - further instruction and an Authorization Letter template. -

+ +
    +
  1. the parcel under application is owned by more than one person;
  2. +
  3. the parcel(s) is owned by an organization; or
  4. +
  5. the parcel(s) is owned by a corporation (private, Crown, local government, First Nations); or
  6. +
  7. the application is being submitted by a third-party agent on behalf of the land owner(s)
  8. +
+

+ The authorization letter must be signed by all individual land owners and the majority of directors in + organization land owners listed in Step 1. Please consult the Supporting Documentation page of ALC website for + further instruction and an Authorization Letter template. +

+
+ + An authorization letter must be provided only if the application is being submitted by a third-party agent. Please + consult the Supporting Documentation page of the TODO: FIX THIS: ALC website for further instruction. +
-
Authorization Letters
+
Authorization Letters (if applicable)
{ let mockAppService: DeepMocked; let mockAppDocumentService: DeepMocked; let mockAppOwnerService: DeepMocked; + let mockAuthService: DeepMocked; let applicationDocumentPipe = new BehaviorSubject([]); @@ -24,6 +27,9 @@ describe('PrimaryContactComponent', () => { mockAppService = createMock(); mockAppDocumentService = createMock(); mockAppOwnerService = createMock(); + mockAuthService = createMock(); + + mockAuthService.$currentProfile = new BehaviorSubject(undefined); await TestBed.configureTestingModule({ providers: [ @@ -43,6 +49,10 @@ describe('PrimaryContactComponent', () => { provide: MatDialog, useValue: {}, }, + { + provide: AuthenticationService, + useValue: mockAuthService, + }, ], declarations: [PrimaryContactComponent], schemas: [NO_ERRORS_SCHEMA], diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index 4e8871f591..71d0d984a6 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -1,19 +1,16 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; -import { BehaviorSubject, takeUntil } from 'rxjs'; +import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, DOCUMENT_TYPE } 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 { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -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 { AuthenticationService } from '../../../services/authentication/authentication.service'; import { EditApplicationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; -import { StepComponent } from '../step.partial'; @Component({ selector: 'app-primary-contact', @@ -23,14 +20,17 @@ import { StepComponent } from '../step.partial'; export class PrimaryContactComponent extends FilesStepComponent implements OnInit, OnDestroy { currentStep = EditApplicationSteps.PrimaryContact; - nonAgentOwners: ApplicationOwnerDto[] = []; + parcelOwners: ApplicationOwnerDto[] = []; owners: ApplicationOwnerDto[] = []; files: (ApplicationDocumentDto & { errorMessage?: string })[] = []; needsAuthorizationLetter = false; selectedThirdPartyAgent = false; + selectedLocalGovernment = false; selectedOwnerUuid: string | undefined = undefined; isCrownOwner = false; + isLocalGovernmentUser = false; + governmentName: string | undefined; firstName = new FormControl('', [Validators.required]); lastName = new FormControl('', [Validators.required]); @@ -53,6 +53,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni private applicationService: ApplicationSubmissionService, applicationDocumentService: ApplicationDocumentService, private applicationOwnerService: ApplicationOwnerService, + private authenticationService: AuthenticationService, dialog: MatDialog ) { super(applicationDocumentService, dialog); @@ -67,6 +68,14 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } }); + this.authenticationService.$currentProfile.pipe(takeUntil(this.$destroy)).subscribe((profile) => { + this.isLocalGovernmentUser = !!profile?.government; + this.governmentName = profile?.government; + if (this.isLocalGovernmentUser) { + this.prepareGovernmentOwners(); + } + }); + this.$applicationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { this.files = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.AUTHORIZATION_LETTER); }); @@ -76,41 +85,28 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni await this.save(); } - protected async save() { - let selectedOwner: ApplicationOwnerDto | undefined = this.owners.find( - (owner) => owner.uuid === this.selectedOwnerUuid - ); + onSelectAgent() { + this.onSelectOwner('agent'); + } - if (this.selectedThirdPartyAgent) { - await this.applicationOwnerService.setPrimaryContact({ - applicationSubmissionUuid: this.submissionUuid, - agentOrganization: this.organizationName.getRawValue() ?? '', - agentFirstName: this.firstName.getRawValue() ?? '', - agentLastName: this.lastName.getRawValue() ?? '', - agentEmail: this.email.getRawValue() ?? '', - agentPhoneNumber: this.phoneNumber.getRawValue() ?? '', - ownerUuid: selectedOwner?.uuid, - }); - } else if (selectedOwner) { - await this.applicationOwnerService.setPrimaryContact({ - applicationSubmissionUuid: this.submissionUuid, - ownerUuid: selectedOwner.uuid, - }); - } + onSelectGovernment() { + this.onSelectOwner('government'); } onSelectOwner(uuid: string) { this.selectedOwnerUuid = uuid; - const selectedOwner = this.nonAgentOwners.find((owner) => owner.uuid === uuid); - this.nonAgentOwners = this.nonAgentOwners.map((owner) => ({ + const selectedOwner = this.parcelOwners.find((owner) => owner.uuid === uuid); + this.parcelOwners = this.parcelOwners.map((owner) => ({ ...owner, isSelected: owner.uuid === uuid, })); - const hasSelectedAgent = (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.AGENT) || uuid == 'agent'; - this.selectedThirdPartyAgent = hasSelectedAgent; + this.selectedThirdPartyAgent = + (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.AGENT) || uuid == 'agent'; + this.selectedLocalGovernment = + (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.GOVERNMENT) || uuid == 'government'; this.form.reset(); - if (hasSelectedAgent) { + if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { this.firstName.enable(); this.lastName.enable(); this.organizationName.enable(); @@ -135,10 +131,17 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni this.isCrownOwner = selectedOwner.type.code === APPLICATION_OWNER.CROWN; } } + + const isSelfApplicant = + this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || + this.owners[0].type.code === APPLICATION_OWNER.GOVERNMENT; + this.needsAuthorizationLetter = !( - this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL && + isSelfApplicant && (this.owners.length === 1 || - (this.owners.length === 2 && this.owners[1].type.code === APPLICATION_OWNER.AGENT && !hasSelectedAgent)) + (this.owners.length === 2 && + this.owners[1].type.code === APPLICATION_OWNER.AGENT && + !this.selectedThirdPartyAgent)) ); this.files = this.files.map((file) => ({ ...file, @@ -148,20 +151,46 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni })); } - onSelectAgent() { - this.onSelectOwner('agent'); + protected async save() { + let selectedOwner: ApplicationOwnerDto | undefined = this.owners.find( + (owner) => owner.uuid === this.selectedOwnerUuid + ); + + if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { + await this.applicationOwnerService.setPrimaryContact({ + applicationSubmissionUuid: this.submissionUuid, + organization: this.organizationName.getRawValue() ?? '', + firstName: this.firstName.getRawValue() ?? '', + lastName: this.lastName.getRawValue() ?? '', + email: this.email.getRawValue() ?? '', + phoneNumber: this.phoneNumber.getRawValue() ?? '', + ownerUuid: selectedOwner?.uuid, + type: this.selectedThirdPartyAgent ? APPLICATION_OWNER.AGENT : APPLICATION_OWNER.GOVERNMENT, + }); + } else if (selectedOwner) { + await this.applicationOwnerService.setPrimaryContact({ + applicationSubmissionUuid: this.submissionUuid, + ownerUuid: selectedOwner.uuid, + }); + } } private async loadOwners(submissionUuid: string, primaryContactOwnerUuid?: string) { const owners = await this.applicationOwnerService.fetchBySubmissionId(submissionUuid); if (owners) { const selectedOwner = owners.find((owner) => owner.uuid === primaryContactOwnerUuid); - this.nonAgentOwners = owners.filter((owner) => owner.type.code !== APPLICATION_OWNER.AGENT); + this.parcelOwners = owners.filter( + (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + ); this.owners = owners; - if (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.AGENT) { + if (selectedOwner) { + this.selectedThirdPartyAgent = selectedOwner.type.code === APPLICATION_OWNER.AGENT; + this.selectedLocalGovernment = selectedOwner.type.code === APPLICATION_OWNER.GOVERNMENT; + } + + if (selectedOwner && (this.selectedThirdPartyAgent || this.selectedLocalGovernment)) { this.selectedOwnerUuid = selectedOwner.uuid; - this.selectedThirdPartyAgent = true; this.form.patchValue({ firstName: selectedOwner.firstName, lastName: selectedOwner.lastName, @@ -179,9 +208,17 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni this.phoneNumber.disable(); } + if (this.isLocalGovernmentUser) { + this.prepareGovernmentOwners(); + } + if (this.showErrors) { this.form.markAllAsTouched(); } } } + + private prepareGovernmentOwners() { + this.parcelOwners = []; + } } 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 c053632910..090698c4d0 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 @@ -7,6 +7,7 @@ export enum APPLICATION_OWNER { ORGANIZATION = 'ORGZ', AGENT = 'AGEN', CROWN = 'CRWN', + GOVERNMENT = 'GOVR', } export interface ApplicationOwnerTypeDto extends BaseCodeDto { @@ -45,11 +46,12 @@ export interface ApplicationOwnerCreateDto extends ApplicationOwnerUpdateDto { } export interface SetPrimaryContactDto { - agentFirstName?: string; - agentLastName?: string; - agentOrganization?: string; - agentPhoneNumber?: string; - agentEmail?: string; + firstName?: string; + lastName?: string; + organization?: string; + phoneNumber?: string; + email?: string; + type?: APPLICATION_OWNER; ownerUuid?: string; applicationSubmissionUuid: string; } diff --git a/portal-frontend/src/app/services/authentication/auth-interceptor.service.spec.ts b/portal-frontend/src/app/services/authentication/auth-interceptor.service.spec.ts index 372ca00461..4c59fd8cc1 100644 --- a/portal-frontend/src/app/services/authentication/auth-interceptor.service.spec.ts +++ b/portal-frontend/src/app/services/authentication/auth-interceptor.service.spec.ts @@ -13,7 +13,7 @@ describe('AuthInterceptorService', () => { beforeEach(() => { mockAuthService = createMock(); - mockAuthService.$currentUser = new BehaviorSubject(undefined); + mockAuthService.$currentTokenUser = new BehaviorSubject(undefined); TestBed.configureTestingModule({ providers: [ diff --git a/portal-frontend/src/app/services/authentication/authentication.dto.ts b/portal-frontend/src/app/services/authentication/authentication.dto.ts new file mode 100644 index 0000000000..c62bc621f6 --- /dev/null +++ b/portal-frontend/src/app/services/authentication/authentication.dto.ts @@ -0,0 +1,10 @@ +export interface UserDto { + uuid: string; + initials: string; + name: string; + identityProvider: string; + idirUserName?: string | null; + bceidUserName?: string | null; + prettyName?: string | null; + government?: string; +} diff --git a/portal-frontend/src/app/services/authentication/authentication.service.ts b/portal-frontend/src/app/services/authentication/authentication.service.ts index 4705cf01d5..f531c799c1 100644 --- a/portal-frontend/src/app/services/authentication/authentication.service.ts +++ b/portal-frontend/src/app/services/authentication/authentication.service.ts @@ -4,6 +4,7 @@ import { Router } from '@angular/router'; import jwtDecode, { JwtPayload } from 'jwt-decode'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; +import { UserDto } from './authentication.dto'; const JWT_TOKEN_KEY = 'jwt_token'; const REFRESH_TOKEN_KEY = 'refresh_token'; @@ -23,7 +24,8 @@ export class AuthenticationService { private refreshExpires: number | undefined; isInitialized = false; - $currentUser = new BehaviorSubject(undefined); + $currentTokenUser = new BehaviorSubject(undefined); + $currentProfile = new BehaviorSubject(undefined); currentUser: ICurrentUser | undefined; constructor(private http: HttpClient, private router: Router) {} @@ -41,7 +43,8 @@ export class AuthenticationService { this.refreshExpires = decodedRefreshToken.exp! * 1000; this.expires = decodedToken.exp! * 1000; this.currentUser = decodedToken as ICurrentUser; - this.$currentUser.next(this.currentUser); + this.$currentTokenUser.next(this.currentUser); + this.loadUser(); } clearTokens() { @@ -137,4 +140,9 @@ export class AuthenticationService { private async getLogoutUrl() { return firstValueFrom(this.http.get<{ url: string }>(`${environment.authUrl}/logout/portal`)); } + + private async loadUser() { + const user = await firstValueFrom(this.http.get(`${environment.authUrl}/user/profile`)); + this.$currentProfile.next(user); + } } diff --git a/portal-frontend/src/app/services/authentication/token-refresh.service.ts b/portal-frontend/src/app/services/authentication/token-refresh.service.ts index 1d8177ee0e..96e420c40a 100644 --- a/portal-frontend/src/app/services/authentication/token-refresh.service.ts +++ b/portal-frontend/src/app/services/authentication/token-refresh.service.ts @@ -12,7 +12,7 @@ export class TokenRefreshService { constructor(private authenticationService: AuthenticationService) {} init() { - this.authenticationService.$currentUser.subscribe((user) => { + this.authenticationService.$currentTokenUser.subscribe((user) => { if (user) { if (this.interval) { clearInterval(this.interval); diff --git a/portal-frontend/src/app/shared/header/header.component.spec.ts b/portal-frontend/src/app/shared/header/header.component.spec.ts index bfbbe7d67a..cc8c34d530 100644 --- a/portal-frontend/src/app/shared/header/header.component.spec.ts +++ b/portal-frontend/src/app/shared/header/header.component.spec.ts @@ -12,7 +12,7 @@ describe('HeaderComponent', () => { beforeEach(async () => { mockAuthService = createMock(); - mockAuthService.$currentUser = new BehaviorSubject(undefined); + mockAuthService.$currentTokenUser = new BehaviorSubject(undefined); await TestBed.configureTestingModule({ declarations: [HeaderComponent], diff --git a/portal-frontend/src/app/shared/header/header.component.ts b/portal-frontend/src/app/shared/header/header.component.ts index b695a7cb75..79817826b0 100644 --- a/portal-frontend/src/app/shared/header/header.component.ts +++ b/portal-frontend/src/app/shared/header/header.component.ts @@ -16,7 +16,7 @@ export class HeaderComponent implements OnInit, OnDestroy { constructor(private authenticationService: AuthenticationService, private router: Router) {} ngOnInit(): void { - this.authenticationService.$currentUser.pipe(takeUntil(this.$destroy)).subscribe((user) => { + this.authenticationService.$currentTokenUser.pipe(takeUntil(this.$destroy)).subscribe((user) => { if (user) { this.isAuthenticated = true; } diff --git a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts b/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts index 4d4a126dd4..c9f542e873 100644 --- a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts +++ b/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts @@ -1,7 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsWhere, ILike, Repository } from 'typeorm'; -import { ApplicationSubmissionReview } from '../../../../portal/application-submission-review/application-submission-review.entity'; import { HolidayEntity } from '../../../admin/holiday/holiday.entity'; import { LocalGovernmentCreateDto, diff --git a/services/apps/alcs/src/main.module.ts b/services/apps/alcs/src/main.module.ts index 3200e044c1..045e0d91b7 100644 --- a/services/apps/alcs/src/main.module.ts +++ b/services/apps/alcs/src/main.module.ts @@ -11,7 +11,6 @@ import { ClsModule } from 'nestjs-cls'; import { LoggerModule } from 'nestjs-pino'; import { CdogsModule } from '../../../libs/common/src/cdogs/cdogs.module'; import { AlcsModule } from './alcs/alcs.module'; -import { ApplicationSubmissionStatusModule } from './application-submission-status/application-submission-status.module'; import { AuthorizationFilter } from './common/authorization/authorization.filter'; import { AuthorizationModule } from './common/authorization/authorization.module'; import { AuditSubscriber } from './common/entities/audit.subscriber'; @@ -23,15 +22,13 @@ import { MainController } from './main.controller'; import { MainService } from './main.service'; import { PortalModule } from './portal/portal.module'; import { TypeormConfigService } from './providers/typeorm/typeorm.service'; -import { User } from './user/user.entity'; import { UserModule } from './user/user.module'; -import { UserService } from './user/user.service'; @Module({ imports: [ ConfigModule, TypeOrmModule.forRootAsync({ useClass: TypeormConfigService }), - TypeOrmModule.forFeature([HealthCheck, User]), + TypeOrmModule.forFeature([HealthCheck]), AutomapperModule.forRoot({ strategyInitializer: classes(), }), @@ -69,7 +66,6 @@ import { UserService } from './user/user.service'; controllers: [MainController, LogoutController], providers: [ MainService, - UserService, AuditSubscriber, { provide: APP_GUARD, 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 fdd19fd8df..8aa0a7ddcb 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 @@ -255,7 +255,7 @@ describe('ApplicationOwnerController', () => { }); it('should create a new owner when setting primary contact to third party agent that doesnt exist', async () => { - mockAppOwnerService.deleteAgents.mockResolvedValue([]); + mockAppOwnerService.deleteNonParcelOwners.mockResolvedValue([]); mockAppOwnerService.create.mockResolvedValue(new ApplicationOwner()); mockAppOwnerService.setPrimaryContact.mockResolvedValue(); mockApplicationSubmissionService.verifyAccessByUuid.mockResolvedValue( @@ -271,7 +271,7 @@ describe('ApplicationOwnerController', () => { }, ); - expect(mockAppOwnerService.deleteAgents).toHaveBeenCalledTimes(1); + expect(mockAppOwnerService.deleteNonParcelOwners).toHaveBeenCalledTimes(1); expect(mockAppOwnerService.create).toHaveBeenCalledTimes(1); expect(mockAppOwnerService.setPrimaryContact).toHaveBeenCalledTimes(1); expect( @@ -288,7 +288,7 @@ describe('ApplicationOwnerController', () => { }), ); mockAppOwnerService.setPrimaryContact.mockResolvedValue(); - mockAppOwnerService.deleteAgents.mockResolvedValue({} as any); + mockAppOwnerService.deleteNonParcelOwners.mockResolvedValue({} as any); mockApplicationSubmissionService.verifyAccessByUuid.mockResolvedValue( new ApplicationSubmission(), ); @@ -306,7 +306,7 @@ describe('ApplicationOwnerController', () => { expect( mockApplicationSubmissionService.verifyAccessByUuid, ).toHaveBeenCalledTimes(1); - expect(mockAppOwnerService.deleteAgents).toHaveBeenCalledTimes(1); + expect(mockAppOwnerService.deleteNonParcelOwners).toHaveBeenCalledTimes(1); }); it('should update the agent owner when calling set primary contact', async () => { 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 1349a1d89f..03b0576c78 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 @@ -161,41 +161,44 @@ export class ApplicationOwnerController { //Create Owner if (!data.ownerUuid) { - await this.ownerService.deleteAgents(applicationSubmission); - const agentOwner = await this.ownerService.create( + await this.ownerService.deleteNonParcelOwners(applicationSubmission); + const newOwner = await this.ownerService.create( { - email: data.agentEmail, - typeCode: APPLICATION_OWNER.AGENT, - lastName: data.agentLastName, - firstName: data.agentFirstName, - phoneNumber: data.agentPhoneNumber, - organizationName: data.agentOrganization, + email: data.email, + typeCode: data.type, + lastName: data.lastName, + firstName: data.firstName, + phoneNumber: data.phoneNumber, + organizationName: data.organization, applicationSubmissionUuid: data.applicationSubmissionUuid, }, applicationSubmission, ); await this.ownerService.setPrimaryContact( applicationSubmission.uuid, - agentOwner, + newOwner, ); } else if (data.ownerUuid) { const primaryContactOwner = await this.ownerService.getOwner( data.ownerUuid, ); - if (primaryContactOwner.type.code === APPLICATION_OWNER.AGENT) { - //Update Fields for existing agent + if ( + primaryContactOwner.type.code === APPLICATION_OWNER.AGENT || + primaryContactOwner.type.code === APPLICATION_OWNER.GOVERNMENT + ) { + //Update Fields for non parcel owners await this.ownerService.update(primaryContactOwner.uuid, { - email: data.agentEmail, - typeCode: APPLICATION_OWNER.AGENT, - lastName: data.agentLastName, - firstName: data.agentFirstName, - phoneNumber: data.agentPhoneNumber, - organizationName: data.agentOrganization, + email: data.email, + typeCode: primaryContactOwner.type.code, + lastName: data.lastName, + firstName: data.firstName, + phoneNumber: data.phoneNumber, + organizationName: data.organization, }); } else { - //Delete Agents if we aren't using one - await this.ownerService.deleteAgents(applicationSubmission); + //Delete Non parcel owners if we aren't using one + await this.ownerService.deleteNonParcelOwners(applicationSubmission); } await this.ownerService.setPrimaryContact( 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 09d3377c24..10ec1bdac7 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 @@ -6,7 +6,6 @@ import { IsUUID, Matches, } from 'class-validator'; -import { DOCUMENT_TYPE } from '../../../alcs/application/application-document/application-document-code.entity'; import { ApplicationDocumentDto } from '../../../alcs/application/application-document/application-document.dto'; import { BaseCodeDto } from '../../../common/dtos/base.dto'; import { emailRegex } from '../../../utils/email.helper'; @@ -17,6 +16,7 @@ export enum APPLICATION_OWNER { ORGANIZATION = 'ORGZ', AGENT = 'AGEN', CROWN = 'CRWN', + GOVERNMENT = 'GOVR', } export class ApplicationOwnerTypeDto extends BaseCodeDto {} @@ -97,23 +97,27 @@ export class ApplicationOwnerCreateDto extends ApplicationOwnerUpdateDto { export class SetPrimaryContactDto { @IsString() @IsOptional() - agentFirstName?: string; + firstName?: string; @IsString() @IsOptional() - agentLastName?: string; + lastName?: string; @IsString() @IsOptional() - agentOrganization?: string; + organization?: string; @IsString() @IsOptional() - agentPhoneNumber?: string; + phoneNumber?: string; + + @IsString() + @IsOptional() + email?: string; @IsString() @IsOptional() - agentEmail?: string; + type?: APPLICATION_OWNER; @IsUUID() @IsOptional() 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 b46252a209..51abcd1584 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 @@ -214,16 +214,26 @@ export class ApplicationOwnerService { }); } - async deleteAgents(application: ApplicationSubmission) { + async deleteNonParcelOwners(application: ApplicationSubmission) { const agentOwners = await this.repository.find({ - where: { - applicationSubmission: { - fileNumber: application.fileNumber, + where: [ + { + applicationSubmission: { + fileNumber: application.fileNumber, + }, + type: { + code: APPLICATION_OWNER.AGENT, + }, }, - type: { - code: APPLICATION_OWNER.AGENT, + { + applicationSubmission: { + fileNumber: application.fileNumber, + }, + type: { + code: APPLICATION_OWNER.GOVERNMENT, + }, }, - }, + ], }); return await this.repository.remove(agentOwners); } 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 0d5cd4e44f..d9937bf19c 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 @@ -314,6 +314,40 @@ describe('ApplicationSubmissionValidatorService', () => { ).toBe(false); }); + 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, + }), + firstName: 'Bruce', + lastName: 'Wayne', + }); + + const governmentOwner = new ApplicationOwner({ + uuid: 'government-owner-uuid', + type: new ApplicationOwnerType({ + code: APPLICATION_OWNER.GOVERNMENT, + }), + firstName: 'Govern', + lastName: 'Ment', + }); + + const applicationSubmission = new ApplicationSubmission({ + owners: [mockOwner, governmentOwner], + primaryContactOwnerUuid: governmentOwner.uuid, + }); + + const res = await service.validateSubmission(applicationSubmission); + + expect( + includesError( + res.errors, + new Error(`Application has no authorization letters`), + ), + ).toBe(false); + }); + it('should not have an authorization letter error when one is provided', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', 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 c2238c4a71..4f9305f0e3 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 @@ -243,7 +243,10 @@ export class ApplicationSubmissionValidatorService { applicationSubmission.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL; - if (!onlyHasIndividualOwner) { + const isGovernmentContact = + primaryOwner.type.code === APPLICATION_OWNER.GOVERNMENT; + + if (!onlyHasIndividualOwner && !isGovernmentContact) { const authorizationLetters = documents.filter( (document) => document.type?.code === DOCUMENT_TYPE.AUTHORIZATION_LETTER, @@ -257,7 +260,10 @@ export class ApplicationSubmissionValidatorService { } } - if (primaryOwner.type.code === APPLICATION_OWNER.AGENT) { + if ( + primaryOwner.type.code === APPLICATION_OWNER.AGENT || + isGovernmentContact + ) { if ( !primaryOwner.firstName || !primaryOwner.lastName || diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1689806720586-add_government_owner_type.ts b/services/apps/alcs/src/providers/typeorm/migrations/1689806720586-add_government_owner_type.ts new file mode 100644 index 0000000000..26c7609782 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1689806720586-add_government_owner_type.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addGovernmentOwnerType1689806720586 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO "alcs"."application_owner_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, 'Government', 'GOVR', 'For use by LFNG to select themselves'); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "alcs"."application_owner_type" WHERE "code" = 'GOVR'`, + ); + } +} diff --git a/services/apps/alcs/src/user/user.controller.spec.ts b/services/apps/alcs/src/user/user.controller.spec.ts index be89cfd84c..f4390447e1 100644 --- a/services/apps/alcs/src/user/user.controller.spec.ts +++ b/services/apps/alcs/src/user/user.controller.spec.ts @@ -3,6 +3,7 @@ import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; +import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; import { UserProfile } from '../common/automapper/user.automapper.profile'; import { initMockUserDto, @@ -16,13 +17,13 @@ import { UserService } from './user.service'; describe('UserController', () => { let controller: UserController; - let mockService: DeepMocked; + let mockUserService: DeepMocked; let mockUser: Partial; let mockUserDto: UserDto; let request; beforeEach(async () => { - mockService = createMock(); + mockUserService = createMock(); const module: TestingModule = await Test.createTestingModule({ controllers: [UserController, UserProfile], @@ -35,7 +36,7 @@ describe('UserController', () => { //Keep this below mockKeyCloak as it overrides the one from there { provide: UserService, - useValue: mockService, + useValue: mockUserService, }, ], imports: [ @@ -86,37 +87,37 @@ describe('UserController', () => { expect(controller).toBeDefined(); }); it('should call getAssignableUsers on the service', async () => { - mockService.getAssignableUsers.mockResolvedValue([mockUser as User]); + mockUserService.getAssignableUsers.mockResolvedValue([mockUser as User]); const res = await controller.getAssignableUsers(); expect(res[0].name).toEqual(mockUserDto.name); expect(res[0].initials).toEqual(mockUserDto.initials); - expect(mockService.getAssignableUsers).toHaveBeenCalledTimes(1); + expect(mockUserService.getAssignableUsers).toHaveBeenCalledTimes(1); }); it('should call deleteUser on the service', async () => { - mockService.delete.mockResolvedValue(mockUser as User); + mockUserService.delete.mockResolvedValue(mockUser as User); const res = await controller.deleteUser(''); expect(res).toBeTruthy(); - expect(mockService.delete).toHaveBeenCalledTimes(1); + expect(mockUserService.delete).toHaveBeenCalledTimes(1); }); it('should call update user on the service', async () => { const mockUserDto = initMockUserDto(); - mockService.getByUuid.mockResolvedValueOnce(mockUser as User); - mockService.update.mockResolvedValueOnce({} as any); + mockUserService.getByUuid.mockResolvedValueOnce(mockUser as User); + mockUserService.update.mockResolvedValueOnce({} as any); request.user.entity.uuid = mockUser.uuid = mockUserDto.uuid; await controller.update(mockUserDto.uuid, mockUserDto, request); - expect(mockService.update).toBeCalledTimes(1); + expect(mockUserService.update).toBeCalledTimes(1); }); it('should fail on user update if user not found', async () => { - mockService.getByUuid.mockResolvedValueOnce(null); + mockUserService.getByUuid.mockResolvedValueOnce(null); const mockUserDto = initMockUserDto(); request.user.entity.uuid = mockUser.uuid = mockUserDto.uuid; @@ -129,8 +130,8 @@ describe('UserController', () => { it('should fail on user update if current user does not mach updating user', async () => { const mockUserDto = initMockUserDto(); - mockService.getByUuid.mockResolvedValueOnce(mockUser as User); - mockService.update.mockResolvedValueOnce({} as any); + mockUserService.getByUuid.mockResolvedValueOnce(mockUser as User); + mockUserService.update.mockResolvedValueOnce({} as any); await expect( controller.update(mockUserDto.uuid, mockUserDto, request), @@ -141,6 +142,13 @@ describe('UserController', () => { it('return the current user', async () => { const mockEntity = initUserMockEntity(); + const governmentName = 'Government'; + + mockUserService.getUserLocalGovernment.mockResolvedValue( + new ApplicationLocalGovernment({ + name: governmentName, + }), + ); const res = await controller.getMyself({ user: { @@ -148,6 +156,7 @@ describe('UserController', () => { }, }); expect(res.name).toEqual(mockEntity.name); + expect(res.government).toEqual(governmentName); expect(res.identityProvider).toEqual(mockEntity.identityProvider); }); }); diff --git a/services/apps/alcs/src/user/user.controller.ts b/services/apps/alcs/src/user/user.controller.ts index be0737da54..f7cf70c275 100644 --- a/services/apps/alcs/src/user/user.controller.ts +++ b/services/apps/alcs/src/user/user.controller.ts @@ -31,10 +31,13 @@ export class UserController { ) {} @Get('/profile') - @UserRoles(...ANY_AUTH_ROLE) + @UserRoles() async getMyself(@Req() req) { const user = req.user.entity; - return this.userMapper.mapAsync(user, User, UserDto); + const mappedUser = await this.userMapper.mapAsync(user, User, UserDto); + const government = await this.userService.getUserLocalGovernment(user); + mappedUser.government = government ? government.name : undefined; + return mappedUser; } @Get('/assignable') diff --git a/services/apps/alcs/src/user/user.dto.ts b/services/apps/alcs/src/user/user.dto.ts index 9c1f895d7d..4b768f00d7 100644 --- a/services/apps/alcs/src/user/user.dto.ts +++ b/services/apps/alcs/src/user/user.dto.ts @@ -38,6 +38,8 @@ export class UserDto extends UpdateUserDto { @AutoMap() prettyName?: string | null; + + government?: string; } export class CreateUserDto { diff --git a/services/apps/alcs/src/user/user.module.ts b/services/apps/alcs/src/user/user.module.ts index 7acae6e908..9269c64bdb 100644 --- a/services/apps/alcs/src/user/user.module.ts +++ b/services/apps/alcs/src/user/user.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; import { UserProfile } from '../common/automapper/user.automapper.profile'; import { EmailModule } from '../providers/email/email.module'; import { UserController } from './user.controller'; @@ -7,7 +8,10 @@ import { User } from './user.entity'; import { UserService } from './user.service'; @Module({ - imports: [TypeOrmModule.forFeature([User]), EmailModule], + imports: [ + TypeOrmModule.forFeature([ApplicationLocalGovernment, User]), + EmailModule, + ], providers: [UserService, UserProfile], exports: [UserService, EmailModule], controllers: [UserController], diff --git a/services/apps/alcs/src/user/user.service.spec.ts b/services/apps/alcs/src/user/user.service.spec.ts index aa3fc94031..fe94069eb8 100644 --- a/services/apps/alcs/src/user/user.service.spec.ts +++ b/services/apps/alcs/src/user/user.service.spec.ts @@ -8,6 +8,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import * as config from 'config'; import { Repository } from 'typeorm'; import { initUserMockEntity } from '../../test/mocks/mockEntities'; +import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; import { UserProfile } from '../common/automapper/user.automapper.profile'; import { EmailService } from '../providers/email/email.service'; import { User } from './user.entity'; @@ -15,7 +16,10 @@ import { UserService } from './user.service'; describe('UserService', () => { let service: UserService; - let repositoryMock = createMock>(); + let mockUserRepository: DeepMocked>; + let mockGovernmentRepository: DeepMocked< + Repository + >; let emailServiceMock: DeepMocked; const email = 'bruce.wayne@gotham.com'; @@ -23,14 +27,20 @@ describe('UserService', () => { mockUser.email = email; beforeEach(async () => { - emailServiceMock = createMock(); + emailServiceMock = createMock(); + mockUserRepository = createMock(); + mockGovernmentRepository = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, { provide: getRepositoryToken(User), - useValue: repositoryMock, + useValue: mockUserRepository, + }, + { + provide: getRepositoryToken(ApplicationLocalGovernment), + useValue: mockGovernmentRepository, }, { provide: EmailService, useValue: emailServiceMock }, UserProfile, @@ -46,13 +56,13 @@ describe('UserService', () => { ], }).compile(); - repositoryMock = module.get(getRepositoryToken(User)); + mockUserRepository = module.get(getRepositoryToken(User)); service = module.get(UserService); - repositoryMock.findOne.mockResolvedValue(mockUser); - repositoryMock.save.mockResolvedValue(mockUser); - repositoryMock.find.mockResolvedValue([mockUser]); - repositoryMock.softRemove.mockResolvedValue(mockUser); + mockUserRepository.findOne.mockResolvedValue(mockUser); + mockUserRepository.save.mockResolvedValue(mockUser); + mockUserRepository.find.mockResolvedValue([mockUser]); + mockUserRepository.softRemove.mockResolvedValue(mockUser); emailServiceMock.sendEmail.mockResolvedValue(); }); @@ -72,12 +82,12 @@ describe('UserService', () => { describe('createUser', () => { it('should save a user when user does not exist', async () => { - repositoryMock.findOne.mockResolvedValue(null); + mockUserRepository.findOne.mockResolvedValue(null); const user = await service.create(mockUser); expect(user).toEqual(mockUser); - expect(repositoryMock.save).toHaveBeenCalledTimes(1); + expect(mockUserRepository.save).toHaveBeenCalledTimes(1); }); it('should reject if user already exists', async () => { @@ -91,12 +101,12 @@ describe('UserService', () => { it('should call delete user on the repository', async () => { await service.delete(mockUser.uuid); - expect(repositoryMock.softRemove).toHaveBeenCalledTimes(1); - expect(repositoryMock.softRemove).toHaveBeenCalledWith(mockUser); + expect(mockUserRepository.softRemove).toHaveBeenCalledTimes(1); + expect(mockUserRepository.softRemove).toHaveBeenCalledWith(mockUser); }); it('should reject when user does not exist', async () => { - repositoryMock.findOne.mockResolvedValue(null); + mockUserRepository.findOne.mockResolvedValue(null); await expect(service.delete(mockUser.uuid)).rejects.toMatchObject( new Error(`User with provided uuid ${mockUser.uuid} was not found`), @@ -116,7 +126,7 @@ describe('UserService', () => { }); it('should fail when user does not exist', async () => { - repositoryMock.findOne.mockResolvedValue(null); + mockUserRepository.findOne.mockResolvedValue(null); await expect(service.update('fake-uuid', mockUser)).rejects.toMatchObject( new ServiceNotFoundException(`User not found fake-uuid`), @@ -130,7 +140,7 @@ describe('UserService', () => { const prefix = env === 'production' ? '' : `[${env}]`; const subject = `${prefix} Access Requested to ALCS`; const body = `A new user ${email}: ${userIdentifier} has requested access to ALCS.
-CSS`; +CSS`; await service.sendNewUserRequestEmail(email, userIdentifier); @@ -140,4 +150,30 @@ describe('UserService', () => { subject, }); }); + + it('should not call repository if user does not have a bc business guid', async () => { + mockGovernmentRepository.findOne.mockResolvedValue( + new ApplicationLocalGovernment(), + ); + + const res = await service.getUserLocalGovernment(new User()); + + expect(res).toBeUndefined(); + expect(mockGovernmentRepository.findOne).toHaveBeenCalledTimes(0); + }); + + it('should call repository if user has a bc business guid', async () => { + mockGovernmentRepository.findOne.mockResolvedValue( + new ApplicationLocalGovernment(), + ); + + const res = await service.getUserLocalGovernment( + new User({ + bceidBusinessGuid: 'guid', + }), + ); + + expect(res).toBeDefined(); + expect(mockGovernmentRepository.findOne).toHaveBeenCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/user/user.service.ts b/services/apps/alcs/src/user/user.service.ts index 3a66b707f1..d5a9e99450 100644 --- a/services/apps/alcs/src/user/user.service.ts +++ b/services/apps/alcs/src/user/user.service.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { IConfig } from 'config'; import { Repository } from 'typeorm'; +import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; import { EmailService } from '../providers/email/email.service'; import { CreateUserDto } from './user.dto'; import { User } from './user.entity'; @@ -22,6 +23,8 @@ export class UserService { private userRepository: Repository, @InjectMapper() private userMapper: Mapper, private emailService: EmailService, + @InjectRepository(ApplicationLocalGovernment) + private localGovernmentRepository: Repository, @Inject(CONFIG_TOKEN) private config: IConfig, ) {} @@ -88,12 +91,23 @@ export class UserService { return this.userRepository.save(updatedUser); } + async getUserLocalGovernment(user: User) { + if (user.bceidBusinessGuid) { + return await this.localGovernmentRepository.findOne({ + where: { bceidBusinessGuid: user.bceidBusinessGuid }, + select: { + name: true, + }, + }); + } + } + async sendNewUserRequestEmail(email: string, userIdentifier: string) { const env = this.config.get('ENV'); const prefix = env === 'production' ? '' : `[${env}]`; const subject = `${prefix} Access Requested to ALCS`; const body = `A new user ${email}: ${userIdentifier} has requested access to ALCS.
-CSS`; +CSS`; await this.emailService.sendEmail({ to: this.config.get('EMAIL.DEFAULT_ADMINS'), From e6f2b76cc7a4e3389b069cfb44bfdb24e446d28a Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 20 Jul 2023 12:26:06 -0700 Subject: [PATCH 2/2] Code Review Feedback * Add test for deleteNonParcelOwners * Change to use submission uuid to not impact draft/non-draft submissions --- .../application-owner/application-owner.controller.ts | 6 ++++-- .../application-owner.service.spec.ts | 10 ++++++++++ .../application-owner/application-owner.service.ts | 6 +++--- 3 files changed, 17 insertions(+), 5 deletions(-) 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 03b0576c78..4a900a8353 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 @@ -161,7 +161,7 @@ export class ApplicationOwnerController { //Create Owner if (!data.ownerUuid) { - await this.ownerService.deleteNonParcelOwners(applicationSubmission); + await this.ownerService.deleteNonParcelOwners(applicationSubmission.uuid); const newOwner = await this.ownerService.create( { email: data.email, @@ -198,7 +198,9 @@ export class ApplicationOwnerController { }); } else { //Delete Non parcel owners if we aren't using one - await this.ownerService.deleteNonParcelOwners(applicationSubmission); + await this.ownerService.deleteNonParcelOwners( + applicationSubmission.uuid, + ); } await this.ownerService.setPrimaryContact( 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 48ef867b65..b78ca5feb0 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 @@ -329,4 +329,14 @@ describe('ApplicationOwnerService', () => { 'A et al.', ); }); + + it('should load then delete non application owners', async () => { + mockRepo.find.mockResolvedValue([new ApplicationOwner()]); + 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/application-submission/application-owner/application-owner.service.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts index 51abcd1584..ddc745c66a 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 @@ -214,12 +214,12 @@ export class ApplicationOwnerService { }); } - async deleteNonParcelOwners(application: ApplicationSubmission) { + async deleteNonParcelOwners(submissionUuid: string) { const agentOwners = await this.repository.find({ where: [ { applicationSubmission: { - fileNumber: application.fileNumber, + uuid: submissionUuid, }, type: { code: APPLICATION_OWNER.AGENT, @@ -227,7 +227,7 @@ export class ApplicationOwnerService { }, { applicationSubmission: { - fileNumber: application.fileNumber, + uuid: submissionUuid, }, type: { code: APPLICATION_OWNER.GOVERNMENT,