From 1525026e618125c44c5cd6a66051b23f827afe8d Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Thu, 23 Nov 2023 16:38:40 -0800 Subject: [PATCH 01/24] Added check status logic when updating acq and related entities | psp-7006 (#3602) * Added status checking to details, take, compensation and other acq file pages * Updated backend and updated tests * Fixed lint * Refactored solver to be simpler * Added tests * Pr comments * Added the sys-admin to edit acquisition fields --- .../DocumentRelationshipController.cs | 4 +- .../api/Constants/AcquisitionStatusTypes.cs | 31 +++ .../api/Constants/AgreementStatusTypes.cs | 19 ++ .../api/Services/AcquisitionFileService.cs | 78 +++++- .../CompensationRequisitionService.cs | 54 ++-- source/backend/api/Services/TakeService.cs | 20 +- .../api/Solvers/AcquisitionStatusSolver.cs | 160 ++++++++++++ .../api/Solvers/IAcquisitionStatusSolver.cs | 19 ++ source/backend/api/Startup.cs | 1 + .../Repositories/AcquisitionFileRepository.cs | 14 +- .../dal/Repositories/AgreementRepository.cs | 2 +- .../Interfaces/IAcquisitionFileRepository.cs | 2 + .../Interfaces/IAgreementRepository.cs | 2 +- source/backend/entities/Partials/Agreement.cs | 23 ++ source/backend/tests/core/TestHelper.cs | 3 + .../Services/AcquisitionFileServiceTest.cs | 177 +++++++++++-- .../CompensationRequisitionServiceTest.cs | 183 +++++++++---- .../unit/api/Services/TakeServiceTest.cs | 36 ++- .../Solvers/AcquisitionStatusSolverTests.cs | 232 +++++++++++++++++ .../src/constants/acquisitionFileStatus.ts | 9 + .../hooks/useAcquisitionFileExport.ts | 2 +- .../AcquisitionFilter.test.tsx | 2 +- .../AcquisitionFilter/AcquisitionFilter.tsx | 12 +- .../acquisition/list/AcquisitionListView.tsx | 2 +- .../AcquisitionSearchResults.test.tsx | 2 +- .../list/AcquisitionSearchResults/columns.tsx | 8 +- .../list/AcquisitionSearchResults/models.ts | 4 +- .../agreement/update/AgreementSubForm.tsx | 18 +- .../update/UpdateAgreementsContainer.tsx | 15 +- .../update/UpdateAgreementsForm.test.tsx | 22 ++ .../agreement/update/UpdateAgreementsForm.tsx | 61 +++-- .../detail/AcquisitionChecklistView.tsx | 5 +- ...CompensationRequisitionDetailContainer.tsx | 3 +- ...CompensationRequisitionDetailView.test.tsx | 22 +- .../CompensationRequisitionDetailView.tsx | 34 ++- ...nsationRequisitionDetailView.test.tsx.snap | 24 +- .../list/CompensationListContainer.tsx | 2 +- .../list/CompensationListView.test.tsx | 15 +- .../list/CompensationListView.tsx | 13 +- .../compensation/list/CompensationResults.tsx | 4 +- .../CompensationListView.test.tsx.snap | 2 +- .../tabs/compensation/list/columns.tsx | 23 +- .../form8/update/UpdateForm8Container.tsx | 4 +- .../detail/AcquisitionSummaryView.tsx | 27 +- .../fileDetails/detail/statusUpdateSolver.ts | 245 ++++++++++++++++++ .../detail/StakeHolderContainer.test.tsx | 104 +------- .../detail/StakeHolderContainer.tsx | 68 +---- .../detail/StakeHolderView.test.tsx | 76 ++++-- .../stakeholders/detail/StakeHolderView.tsx | 27 +- .../detail/stakeholderOrganizer.test.ts | 130 ++++++++++ .../detail/stakeholderOrganizer.ts | 84 ++++++ 51 files changed, 1742 insertions(+), 387 deletions(-) create mode 100644 source/backend/api/Constants/AcquisitionStatusTypes.cs create mode 100644 source/backend/api/Constants/AgreementStatusTypes.cs create mode 100644 source/backend/api/Solvers/AcquisitionStatusSolver.cs create mode 100644 source/backend/api/Solvers/IAcquisitionStatusSolver.cs create mode 100644 source/backend/tests/unit/api/Solvers/AcquisitionStatusSolverTests.cs create mode 100644 source/frontend/src/constants/acquisitionFileStatus.ts create mode 100644 source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/statusUpdateSolver.ts create mode 100644 source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/stakeholderOrganizer.test.ts create mode 100644 source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/stakeholderOrganizer.ts diff --git a/source/backend/api/Areas/Documents/DocumentRelationshipController.cs b/source/backend/api/Areas/Documents/DocumentRelationshipController.cs index c6a038365c..03b5d108d6 100644 --- a/source/backend/api/Areas/Documents/DocumentRelationshipController.cs +++ b/source/backend/api/Areas/Documents/DocumentRelationshipController.cs @@ -92,8 +92,8 @@ public IActionResult GetRelationshipDocuments(DocumentRelationType relationshipT return new JsonResult(mappedResearchFileDocuments); case DocumentRelationType.AcquisitionFiles: var acquistionFileDocuments = _documentFileService.GetFileDocuments(FileType.Acquisition, long.Parse(parentId)); - var mappedAquisitionFileDocuments = _mapper.Map>(acquistionFileDocuments); - return new JsonResult(mappedAquisitionFileDocuments); + var mappedAcquisitionFileDocuments = _mapper.Map>(acquistionFileDocuments); + return new JsonResult(mappedAcquisitionFileDocuments); case DocumentRelationType.Templates: var templateDocuments = _formDocumentService.GetFormDocumentTypes(parentId); var mappedTemplateDocuments = _mapper.Map>(templateDocuments); diff --git a/source/backend/api/Constants/AcquisitionStatusTypes.cs b/source/backend/api/Constants/AcquisitionStatusTypes.cs new file mode 100644 index 0000000000..eeea86e59d --- /dev/null +++ b/source/backend/api/Constants/AcquisitionStatusTypes.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Pims.Api.Constants +{ + [JsonConverter(typeof(JsonStringEnumMemberConverter))] + public enum AcquisitionStatusTypes + { + + [EnumMember(Value = "ACTIVE")] + ACTIVE, + + [EnumMember(Value = "ARCHIV")] + ARCHIV, + + [EnumMember(Value = "CANCEL")] + CANCEL, + + [EnumMember(Value = "CLOSED")] + CLOSED, + + [EnumMember(Value = "COMPLT")] + COMPLT, + + [EnumMember(Value = "DRAFT")] + DRAFT, + + [EnumMember(Value = "HOLD")] + HOLD, + } +} diff --git a/source/backend/api/Constants/AgreementStatusTypes.cs b/source/backend/api/Constants/AgreementStatusTypes.cs new file mode 100644 index 0000000000..c8ffe88bc3 --- /dev/null +++ b/source/backend/api/Constants/AgreementStatusTypes.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Pims.Api.Constants +{ + [JsonConverter(typeof(JsonStringEnumMemberConverter))] + public enum AgreementStatusTypes + { + [EnumMember(Value = "CANCELLED")] + CANCELLED, + + [EnumMember(Value = "DRAFT")] + DRAFT, + + [EnumMember(Value = "FINAL")] + FINAL, + + } +} diff --git a/source/backend/api/Services/AcquisitionFileService.cs b/source/backend/api/Services/AcquisitionFileService.cs index 5c3746e905..ceafa720a7 100644 --- a/source/backend/api/Services/AcquisitionFileService.cs +++ b/source/backend/api/Services/AcquisitionFileService.cs @@ -4,6 +4,7 @@ using System.Security.Claims; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Pims.Api.Constants; using Pims.Api.Helpers.Exceptions; using Pims.Api.Helpers.Extensions; using Pims.Core.Exceptions; @@ -40,6 +41,7 @@ public class AcquisitionFileService : IAcquisitionFileService private readonly ICompReqFinancialService _compReqFinancialService; private readonly IExpropriationPaymentRepository _expropriationPaymentRepository; private readonly ITakeRepository _takeRepository; + private readonly IAcquisitionStatusSolver _statusSolver; public AcquisitionFileService( ClaimsPrincipal user, @@ -57,7 +59,8 @@ public AcquisitionFileService( IInterestHolderRepository interestHolderRepository, ICompReqFinancialService compReqFinancialService, IExpropriationPaymentRepository expropriationPaymentRepository, - ITakeRepository takeRepository) + ITakeRepository takeRepository, + IAcquisitionStatusSolver statusSolver) { _user = user; _logger = logger; @@ -75,6 +78,7 @@ public AcquisitionFileService( _compReqFinancialService = compReqFinancialService; _expropriationPaymentRepository = expropriationPaymentRepository; _takeRepository = takeRepository; + _statusSolver = statusSolver; } public Paged GetPage(AcquisitionFilter filter) @@ -240,6 +244,12 @@ public PimsAcquisitionFile Update(PimsAcquisitionFile acquisitionFile, IEnumerab ValidateVersion(acquisitionFile.Internal_Id, acquisitionFile.ConcurrencyControlNumber); ValidateDrafts(acquisitionFile.Internal_Id); + AcquisitionStatusTypes? currentAcquisitionStatus = GetCurrentAcquisitionStatus(acquisitionFile.Internal_Id); + if (!_statusSolver.CanEditDetails(currentAcquisitionStatus) && !_user.HasPermission(Permissions.SystemAdmin)) + { + throw new BusinessRuleViolationException("The file you are editing is not active or draft, so you cannot save changes. Refresh your browser to see file state."); + } + if (!userOverrides.Contains(UserOverrideCode.UpdateRegion)) { ValidateMinistryRegion(acquisitionFile.Internal_Id, acquisitionFile.RegionCode); @@ -341,6 +351,12 @@ public PimsAcquisitionFile UpdateChecklistItems(PimsAcquisitionFile acquisitionF _user.ThrowIfNotAuthorized(Permissions.AcquisitionFileEdit); _user.ThrowInvalidAccessToAcquisitionFile(_userRepository, _acqFileRepository, acquisitionFile.Internal_Id); + var currentAcquisitionStatus = GetCurrentAcquisitionStatus(acquisitionFile.Internal_Id); + if (!_statusSolver.CanEditChecklists(currentAcquisitionStatus) && !_user.HasPermission(Permissions.SystemAdmin)) + { + throw new BusinessRuleViolationException("The file you are editing is not active or draft, so you cannot save changes. Refresh your browser to see file state."); + } + // Get the current checklist items for this acquisition file. var currentItems = _checklistRepository.GetAllChecklistItemsByAcquisitionFileId(acquisitionFile.Internal_Id).ToDictionary(ci => ci.Internal_Id); @@ -372,7 +388,7 @@ public IEnumerable GetAgreements(long id) _user.ThrowIfNotAuthorized(Permissions.AgreementView); _user.ThrowInvalidAccessToAcquisitionFile(_userRepository, _acqFileRepository, id); - return _agreementRepository.GetAgreementsByAquisitionFile(id); + return _agreementRepository.GetAgreementsByAcquisitionFile(id); } public IEnumerable SearchAgreements(AcquisitionReportFilterModel filter) @@ -393,6 +409,31 @@ public IEnumerable UpdateAgreements(long acquisitionFileId, List< { _user.ThrowInvalidAccessToAcquisitionFile(_userRepository, _acqFileRepository, acquisitionFileId); + var currentAcquisitionStatus = GetCurrentAcquisitionStatus(acquisitionFileId); + + var currentAgreements = _agreementRepository.GetAgreementsByAcquisitionFile(acquisitionFileId); + + var toBeUpdated = currentAgreements.Where(ca => agreements.Any(na => ca.AgreementId == na.AgreementId && !ca.IsEqual(na))); + var toBeDeleted = currentAgreements.Where(ca => !agreements.Any(na => ca.AgreementId == na.AgreementId)); + + foreach (var agreement in toBeUpdated) + { + var agreementStatus = Enum.Parse(agreement.AgreementStatusTypeCode); + if (!_statusSolver.CanEditOrDeleteAgreement(currentAcquisitionStatus, agreementStatus) && !_user.HasPermission(Permissions.SystemAdmin)) + { + throw new BusinessRuleViolationException("The file you are editing is not active or draft, so you cannot save changes. Refresh your browser to see file state."); + } + } + + foreach (var agreement in toBeDeleted) + { + var agreementStatus = Enum.Parse(agreement.AgreementStatusTypeCode); + if (!_statusSolver.CanEditOrDeleteAgreement(currentAcquisitionStatus, agreementStatus) && !_user.HasPermission(Permissions.SystemAdmin)) + { + throw new BusinessRuleViolationException("The file you are editing is not active or draft, so you cannot save changes. Refresh your browser to see file state."); + } + } + var updatedAgreements = _agreementRepository.UpdateAllForAcquisition(acquisitionFileId, agreements); _agreementRepository.CommitTransaction(); @@ -414,6 +455,12 @@ public IEnumerable UpdateInterestHolders(long acquisitionFil _user.ThrowIfNotAuthorized(Permissions.AcquisitionFileEdit); _user.ThrowInvalidAccessToAcquisitionFile(_userRepository, _acqFileRepository, acquisitionFileId); + var currentAcquisitionStatus = GetCurrentAcquisitionStatus(acquisitionFileId); + if (!_statusSolver.CanEditStakeholders(currentAcquisitionStatus) && !_user.HasPermission(Permissions.SystemAdmin)) + { + throw new BusinessRuleViolationException("The file you are editing is not active or draft, so you cannot save changes. Refresh your browser to see file state."); + } + var currentInterestHolders = _interestHolderRepository.GetInterestHoldersByAcquisitionFile(acquisitionFileId); // Verify that the interest holder is still the same (person or org) @@ -690,7 +737,7 @@ private void ValidateMinistryRegion(long acqFileId, short updatedRegion) private void ValidateDrafts(long acqFileId) { - var agreements = _agreementRepository.GetAgreementsByAquisitionFile(acqFileId); + var agreements = _agreementRepository.GetAgreementsByAcquisitionFile(acqFileId); var compensations = _compensationRequisitionRepository.GetAllByAcquisitionFileId(acqFileId); if (agreements.Any(a => a?.AgreementStatusTypeCode == "DRAFT" || compensations.Any(c => c.IsDraft.HasValue && c.IsDraft.Value))) { @@ -838,7 +885,7 @@ private void AppendToAcquisitionChecklist(PimsAcquisitionFile acquisitionFile, r private void ValidatePayeeDependency(PimsAcquisitionFile acquisitionFile) { - var currentAquisitionFile = _acqFileRepository.GetById(acquisitionFile.Internal_Id); + var currentAcquisitionFile = _acqFileRepository.GetById(acquisitionFile.Internal_Id); var compensationRequisitions = _compensationRequisitionRepository.GetAllByAcquisitionFileId(acquisitionFile.Internal_Id); if (compensationRequisitions.Count == 0) @@ -851,7 +898,7 @@ private void ValidatePayeeDependency(PimsAcquisitionFile acquisitionFile) // Check for Acquisition File Owner removed if (compReq.AcquisitionOwnerId is not null && !acquisitionFile.PimsAcquisitionOwners.Any(x => x.Internal_Id.Equals(compReq.AcquisitionOwnerId)) - && currentAquisitionFile.PimsAcquisitionOwners.Any(x => x.Internal_Id.Equals(compReq.AcquisitionOwnerId))) + && currentAcquisitionFile.PimsAcquisitionOwners.Any(x => x.Internal_Id.Equals(compReq.AcquisitionOwnerId))) { throw new ForeignKeyDependencyException("Acquisition File Owner can not be removed since it's assigned as a payee for a compensation requisition"); } @@ -859,7 +906,7 @@ private void ValidatePayeeDependency(PimsAcquisitionFile acquisitionFile) // Check for Acquisition InterestHolders if (compReq.InterestHolderId is not null && !acquisitionFile.PimsInterestHolders.Any(x => x.Internal_Id.Equals(compReq.InterestHolderId)) - && currentAquisitionFile.PimsInterestHolders.Any(x => x.Internal_Id.Equals(compReq.InterestHolderId))) + && currentAcquisitionFile.PimsInterestHolders.Any(x => x.Internal_Id.Equals(compReq.InterestHolderId))) { throw new ForeignKeyDependencyException("Acquisition File Interest Holders can not be removed since it's assigned as a payee for a compensation requisition"); } @@ -867,7 +914,7 @@ private void ValidatePayeeDependency(PimsAcquisitionFile acquisitionFile) // Check for File Person if (compReq.AcquisitionFileTeamId is not null && !acquisitionFile.PimsAcquisitionFileTeams.Any(x => x.Internal_Id.Equals(compReq.AcquisitionFileTeamId)) - && currentAquisitionFile.PimsAcquisitionFileTeams.Any(x => x.Internal_Id.Equals(compReq.AcquisitionFileTeamId))) + && currentAcquisitionFile.PimsAcquisitionFileTeams.Any(x => x.Internal_Id.Equals(compReq.AcquisitionFileTeamId))) { throw new ForeignKeyDependencyException("Acquisition File team member can not be removed since it's assigned as a payee for a compensation requisition"); } @@ -876,7 +923,7 @@ private void ValidatePayeeDependency(PimsAcquisitionFile acquisitionFile) private void ValidateInterestHoldersDependency(long acquisitionFileId, List interestHolders) { - var currentAquisitionFile = _acqFileRepository.GetById(acquisitionFileId); + var currentAcquisitionFile = _acqFileRepository.GetById(acquisitionFileId); var compensationRequisitions = _compensationRequisitionRepository.GetAllByAcquisitionFileId(acquisitionFileId); if (compensationRequisitions.Count == 0) @@ -889,11 +936,24 @@ private void ValidateInterestHoldersDependency(long acquisitionFileId, List x.InterestHolderId.Equals(compReq.InterestHolderId)) - && currentAquisitionFile.PimsInterestHolders.Any(x => x.Internal_Id.Equals(compReq.InterestHolderId))) + && currentAcquisitionFile.PimsInterestHolders.Any(x => x.Internal_Id.Equals(compReq.InterestHolderId))) { throw new ForeignKeyDependencyException("Acquisition File Interest Holder can not be removed since it's assigned as a payee for a compensation requisition"); } } } + + private AcquisitionStatusTypes? GetCurrentAcquisitionStatus(long acquisitionFileId) + { + var currentAcquisitionFile = _acqFileRepository.GetById(acquisitionFileId); + AcquisitionStatusTypes currentAcquisitionStatus; + + if (Enum.TryParse(currentAcquisitionFile.AcquisitionFileStatusTypeCode, out currentAcquisitionStatus)) + { + return currentAcquisitionStatus; + } + + return currentAcquisitionStatus; + } } } diff --git a/source/backend/api/Services/CompensationRequisitionService.cs b/source/backend/api/Services/CompensationRequisitionService.cs index 2ee51c3e16..6b8e8ef755 100644 --- a/source/backend/api/Services/CompensationRequisitionService.cs +++ b/source/backend/api/Services/CompensationRequisitionService.cs @@ -3,10 +3,10 @@ using System.Linq; using System.Security.Claims; using Microsoft.Extensions.Logging; +using Pims.Api.Constants; using Pims.Core.Exceptions; using Pims.Core.Extensions; using Pims.Dal.Entities; -using Pims.Dal.Exceptions; using Pims.Dal.Helpers.Extensions; using Pims.Dal.Repositories; using Pims.Dal.Security; @@ -22,6 +22,7 @@ public class CompensationRequisitionService : ICompensationRequisitionService private readonly IUserRepository _userRepository; private readonly IAcquisitionFileRepository _acqFileRepository; private readonly ICompReqFinancialService _compReqFinancialService; + private readonly IAcquisitionStatusSolver _statusSolver; public CompensationRequisitionService( ClaimsPrincipal user, @@ -30,7 +31,8 @@ public CompensationRequisitionService( IEntityNoteRepository entityNoteRepository, IUserRepository userRepository, IAcquisitionFileRepository acqFileRepository, - ICompReqFinancialService compReqFinancialService) + ICompReqFinancialService compReqFinancialService, + IAcquisitionStatusSolver statusSolver) { _user = user; _logger = logger; @@ -39,6 +41,7 @@ public CompensationRequisitionService( _userRepository = userRepository; _acqFileRepository = acqFileRepository; _compReqFinancialService = compReqFinancialService; + _statusSolver = statusSolver; } public PimsCompensationRequisition GetById(long compensationRequisitionId) @@ -59,8 +62,15 @@ public PimsCompensationRequisition Update(PimsCompensationRequisition compensati var currentCompensation = _compensationRequisitionRepository.GetById(compensationRequisition.CompensationRequisitionId); - CheckDraftStatusUpdateAuthorized(currentCompensation.IsDraft, compensationRequisition.IsDraft); - CheckTotalAllowableCompensation(compensationRequisition.AcquisitionFileId, compensationRequisition); + var currentAcquisitionFile = _acqFileRepository.GetById(currentCompensation.AcquisitionFileId); + var currentAcquisitionStatus = Enum.Parse(currentAcquisitionFile.AcquisitionFileStatusTypeCode); + + if (!_statusSolver.CanEditOrDeleteCompensation(currentAcquisitionStatus, currentCompensation.IsDraft) && !_user.HasPermission(Permissions.SystemAdmin)) + { + throw new BusinessRuleViolationException("The file you are editing is not active or draft, so you cannot save changes. Refresh your browser to see file state."); + } + + CheckTotalAllowableCompensation(currentAcquisitionFile, compensationRequisition); compensationRequisition.FinalizedDate = CheckFinalizedDate(currentCompensation.IsDraft, compensationRequisition.IsDraft, currentCompensation.FinalizedDate); PimsCompensationRequisition updatedEntity = _compensationRequisitionRepository.Update(compensationRequisition); @@ -90,6 +100,15 @@ public bool DeleteCompensation(long compensationId) _logger.LogInformation("Deleting compensation with id ...", compensationId); _user.ThrowIfNotAuthorized(Permissions.CompensationRequisitionDelete, Permissions.AcquisitionFileEdit); + var currentCompensation = _compensationRequisitionRepository.GetById(compensationId); + + var currentAcqusitionStatus = GetCurrentAcquisitionStatus(currentCompensation.AcquisitionFileId); + + if (!_statusSolver.CanEditOrDeleteCompensation(currentAcqusitionStatus, currentCompensation.IsDraft) && !_user.HasPermission(Permissions.SystemAdmin)) + { + throw new BusinessRuleViolationException("The file you are editing is not active or draft, so you cannot save changes. Refresh your browser to see file state."); + } + var fileFormToDelete = _compensationRequisitionRepository.TryDelete(compensationId); _compensationRequisitionRepository.CommitTransaction(); @@ -135,30 +154,27 @@ private void AddNoteIfStatusChanged(long compensationRequisitionId, long acquisi _entityNoteRepository.Add(fileNoteInstance); } - private void CheckDraftStatusUpdateAuthorized(bool? currentStatus, bool? newStatus) + private void CheckTotalAllowableCompensation(PimsAcquisitionFile currentAcquisitionFile, PimsCompensationRequisition newCompensation) { - if (currentStatus.HasValue && currentStatus.Value.Equals(false) - && ((newStatus.HasValue && newStatus.Value.Equals(true)) || !newStatus.HasValue) - && !_user.HasPermission(Permissions.SystemAdmin)) - { - throw new NotAuthorizedException(); - } - } - - private void CheckTotalAllowableCompensation(long currentAcquisitionFileId, PimsCompensationRequisition newCompensation) - { - PimsAcquisitionFile acquisitionFile = _acqFileRepository.GetById(currentAcquisitionFileId); - if (!acquisitionFile.TotalAllowableCompensation.HasValue || (newCompensation.IsDraft.HasValue && newCompensation.IsDraft.Value)) + if (!currentAcquisitionFile.TotalAllowableCompensation.HasValue || (newCompensation.IsDraft.HasValue && newCompensation.IsDraft.Value)) { return; } - IEnumerable allFinancialsForFile = _compReqFinancialService.GetAllByAcquisitionFileId(currentAcquisitionFileId, true); + IEnumerable allFinancialsForFile = _compReqFinancialService.GetAllByAcquisitionFileId(currentAcquisitionFile.AcquisitionFileId, true); IEnumerable allUnchangedFinancialsForFile = allFinancialsForFile.Where(f => f.CompensationRequisitionId != newCompensation.Internal_Id); decimal newTotalCompensation = allUnchangedFinancialsForFile.Concat(newCompensation.PimsCompReqFinancials).Aggregate(0m, (acc, f) => acc + (f.TotalAmt ?? 0m)); - if (newTotalCompensation > acquisitionFile.TotalAllowableCompensation) + if (newTotalCompensation > currentAcquisitionFile.TotalAllowableCompensation) { throw new BusinessRuleViolationException("Your compensation requisition cannot be saved in FINAL status, as its compensation amount exceeds total allowable compensation for this file."); } } + + private AcquisitionStatusTypes GetCurrentAcquisitionStatus(long acquisitionFileId) + { + var currentCompensation = _compensationRequisitionRepository.GetById(acquisitionFileId); + + var currentAcquisitionFile = _acqFileRepository.GetById(currentCompensation.AcquisitionFileId); + return Enum.Parse(currentAcquisitionFile.AcquisitionFileStatusTypeCode); + } } } diff --git a/source/backend/api/Services/TakeService.cs b/source/backend/api/Services/TakeService.cs index 4295a78ad2..712ffe4d63 100644 --- a/source/backend/api/Services/TakeService.cs +++ b/source/backend/api/Services/TakeService.cs @@ -1,6 +1,10 @@ +using System; using System.Collections.Generic; +using System.Linq; using System.Security.Claims; using Microsoft.Extensions.Logging; +using Pims.Api.Constants; +using Pims.Core.Exceptions; using Pims.Dal.Entities; using Pims.Dal.Helpers.Extensions; using Pims.Dal.Repositories; @@ -12,16 +16,22 @@ public class TakeService : ITakeService { private readonly ClaimsPrincipal _user; private readonly ILogger _logger; + private readonly IAcquisitionFileRepository _acqFileRepository; private readonly ITakeRepository _takeRepository; + private readonly IAcquisitionStatusSolver _statusSolver; public TakeService( ClaimsPrincipal user, ILogger logger, - ITakeRepository repository) + IAcquisitionFileRepository acqFileRepository, + ITakeRepository repository, + IAcquisitionStatusSolver statusSolver) { _user = user; _logger = logger; + _acqFileRepository = acqFileRepository; _takeRepository = repository; + _statusSolver = statusSolver; } public IEnumerable GetByFileId(long fileId) @@ -50,6 +60,14 @@ public IEnumerable UpdateAcquisitionPropertyTakes(long acquisitionFile _logger.LogInformation("updating takes with propertyFileId {propertyFileId}", acquisitionFilePropertyId); _user.ThrowIfNotAuthorized(Permissions.PropertyView, Permissions.AcquisitionFileView); + var currentAcquistionFile = _acqFileRepository.GetByAcquisitionFilePropertyId(acquisitionFilePropertyId); + + var currentAcqusitionStatus = Enum.Parse(currentAcquistionFile.AcquisitionFileStatusTypeCode); + if (!_statusSolver.CanEditTakes(currentAcqusitionStatus) && !_user.HasPermission(Permissions.SystemAdmin)) + { + throw new BusinessRuleViolationException("The file you are editing is not active or draft, so you cannot save changes. Refresh your browser to see file state."); + } + _takeRepository.UpdateAcquisitionPropertyTakes(acquisitionFilePropertyId, takes); _takeRepository.CommitTransaction(); diff --git a/source/backend/api/Solvers/AcquisitionStatusSolver.cs b/source/backend/api/Solvers/AcquisitionStatusSolver.cs new file mode 100644 index 0000000000..a49179f70e --- /dev/null +++ b/source/backend/api/Solvers/AcquisitionStatusSolver.cs @@ -0,0 +1,160 @@ +using Pims.Api.Constants; + +namespace Pims.Api.Services +{ + public class AcquisitionStatusSolver : IAcquisitionStatusSolver + { + + public bool CanEditDetails(AcquisitionStatusTypes? acquisitionStatus) + { + if (acquisitionStatus == null) + { + return false; + } + + bool canEdit; + switch (acquisitionStatus) + { + case AcquisitionStatusTypes.ACTIVE: + case AcquisitionStatusTypes.DRAFT: + canEdit = true; + break; + case AcquisitionStatusTypes.ARCHIV: + case AcquisitionStatusTypes.CANCEL: + case AcquisitionStatusTypes.CLOSED: + case AcquisitionStatusTypes.COMPLT: + case AcquisitionStatusTypes.HOLD: + canEdit = false; + break; + default: + canEdit = false; + break; + } + + return canEdit; + } + + public bool CanEditTakes(AcquisitionStatusTypes? acquisitionStatus) + { + if (acquisitionStatus == null) + { + return false; + } + + bool canEdit; + switch (acquisitionStatus) + { + case AcquisitionStatusTypes.ACTIVE: + case AcquisitionStatusTypes.DRAFT: + canEdit = true; + break; + case AcquisitionStatusTypes.ARCHIV: + case AcquisitionStatusTypes.CANCEL: + case AcquisitionStatusTypes.CLOSED: + case AcquisitionStatusTypes.COMPLT: + case AcquisitionStatusTypes.HOLD: + canEdit = false; + break; + default: + canEdit = false; + break; + } + + return canEdit; + } + + public bool CanEditOrDeleteCompensation(AcquisitionStatusTypes? acquisitionStatus, bool? isDraftCompensation) + { + if (acquisitionStatus == null) + { + return false; + } + + bool canEdit; + switch (acquisitionStatus) + { + case AcquisitionStatusTypes.ACTIVE: + case AcquisitionStatusTypes.DRAFT: + canEdit = true; + break; + case AcquisitionStatusTypes.ARCHIV: + case AcquisitionStatusTypes.CANCEL: + case AcquisitionStatusTypes.CLOSED: + case AcquisitionStatusTypes.COMPLT: + case AcquisitionStatusTypes.HOLD: + canEdit = isDraftCompensation ?? true; + break; + default: + canEdit = false; + break; + } + + return canEdit; + } + + public bool CanEditOrDeleteAgreement(AcquisitionStatusTypes? acquisitionStatus, AgreementStatusTypes? agreementStatus) + { + if (acquisitionStatus == null) + { + return false; + } + + bool canEdit; + switch (acquisitionStatus) + { + case AcquisitionStatusTypes.ACTIVE: + case AcquisitionStatusTypes.DRAFT: + canEdit = true; + break; + case AcquisitionStatusTypes.ARCHIV: + case AcquisitionStatusTypes.CANCEL: + case AcquisitionStatusTypes.CLOSED: + case AcquisitionStatusTypes.COMPLT: + case AcquisitionStatusTypes.HOLD: + canEdit = agreementStatus != AgreementStatusTypes.FINAL; + break; + default: + canEdit = false; + break; + } + + return canEdit; + } + + public bool CanEditChecklists(AcquisitionStatusTypes? acquisitionStatus) + { + if (acquisitionStatus == null) + { + return false; + } + + bool canEdit; + switch (acquisitionStatus) + { + default: + canEdit = true; + break; + } + + return canEdit; + } + + public bool CanEditStakeholders(AcquisitionStatusTypes? acquisitionStatus) + { + if (acquisitionStatus == null) + { + return false; + } + + bool canEdit; + switch (acquisitionStatus) + { + default: + canEdit = true; + break; + } + + return canEdit; + } + } +} diff --git a/source/backend/api/Solvers/IAcquisitionStatusSolver.cs b/source/backend/api/Solvers/IAcquisitionStatusSolver.cs new file mode 100644 index 0000000000..cfb04a329d --- /dev/null +++ b/source/backend/api/Solvers/IAcquisitionStatusSolver.cs @@ -0,0 +1,19 @@ +using Pims.Api.Constants; + +namespace Pims.Api.Services +{ + public interface IAcquisitionStatusSolver + { + bool CanEditDetails(AcquisitionStatusTypes? acquisitionStatus); + + bool CanEditTakes(AcquisitionStatusTypes? acquisitionStatus); + + bool CanEditOrDeleteCompensation(AcquisitionStatusTypes? acquisitionStatus, bool? isDraftCompensation); + + bool CanEditOrDeleteAgreement(AcquisitionStatusTypes? acquisitionStatus, AgreementStatusTypes? agreementStatus); + + bool CanEditChecklists(AcquisitionStatusTypes? acquisitionStatus); + + bool CanEditStakeholders(AcquisitionStatusTypes? acquisitionStatus); + } +} \ No newline at end of file diff --git a/source/backend/api/Startup.cs b/source/backend/api/Startup.cs index 4381ca7ba0..298a9b7a95 100644 --- a/source/backend/api/Startup.cs +++ b/source/backend/api/Startup.cs @@ -409,6 +409,7 @@ private static void AddPimsApiServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } /// diff --git a/source/backend/dal/Repositories/AcquisitionFileRepository.cs b/source/backend/dal/Repositories/AcquisitionFileRepository.cs index 3bd2fa870c..c8ea99242c 100644 --- a/source/backend/dal/Repositories/AcquisitionFileRepository.cs +++ b/source/backend/dal/Repositories/AcquisitionFileRepository.cs @@ -53,7 +53,7 @@ public Paged GetPageDeep(AcquisitionFilter filter, HashSet< throw new ArgumentException("Argument must have a valid filter", nameof(filter)); } - IQueryable query = GetCommonAquisitionFileQueryDeep(filter, regions, contractorPersonId); + IQueryable query = GetCommonAcquisitionFileQueryDeep(filter, regions, contractorPersonId); var skip = (filter.Page - 1) * filter.Quantity; var pageItems = query.Skip(skip).Take(filter.Quantity).ToList(); @@ -79,7 +79,7 @@ public List GetAcquisitionFileExportDeep(AcquisitionFilter throw new ArgumentException("Argument must have a valid filter", nameof(filter)); } - return GetCommonAquisitionFileQueryDeep(filter, regions, contractorPersonId).ToList(); + return GetCommonAcquisitionFileQueryDeep(filter, regions, contractorPersonId).ToList(); } /// @@ -692,6 +692,12 @@ public List GetByProductId(long productId) .Where(a => a.ProductId == productId).ToList(); } + public PimsAcquisitionFile GetByAcquisitionFilePropertyId(long acquisitionFilePropertyId) + { + return this.Context.PimsAcquisitionFiles.AsNoTracking() + .FirstOrDefault(a => a.PimsPropertyAcquisitionFiles.Any(x => x.PropertyAcquisitionFileId == acquisitionFilePropertyId)); + } + /// /// Generates a new Acquisition Number in the following format. /// @@ -728,13 +734,13 @@ private int GetNextAcquisitionFileNumberSequenceValue() } /// - /// Generate a Commeon IQueryable for Aquisition Files. + /// Generate a Commeon IQueryable for Acquisition Files. /// /// /// /// /// - private IQueryable GetCommonAquisitionFileQueryDeep(AcquisitionFilter filter, HashSet regions, long? contractorPersonId = null) + private IQueryable GetCommonAcquisitionFileQueryDeep(AcquisitionFilter filter, HashSet regions, long? contractorPersonId = null) { var predicate = PredicateBuilder.New(acq => true); diff --git a/source/backend/dal/Repositories/AgreementRepository.cs b/source/backend/dal/Repositories/AgreementRepository.cs index baa585e3d8..6de5b01081 100644 --- a/source/backend/dal/Repositories/AgreementRepository.cs +++ b/source/backend/dal/Repositories/AgreementRepository.cs @@ -32,7 +32,7 @@ public AgreementRepository(PimsContext dbContext, ClaimsPrincipal user, ILogger< #region Methods - public List GetAgreementsByAquisitionFile(long acquisitionFileId) + public List GetAgreementsByAcquisitionFile(long acquisitionFileId) { using var scope = Logger.QueryScope(); diff --git a/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs b/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs index 99bf04fd21..6c92472232 100644 --- a/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs @@ -26,6 +26,8 @@ public interface IAcquisitionFileRepository : IRepository List GetByProductId(long productId); + PimsAcquisitionFile GetByAcquisitionFilePropertyId(long acquisitionFilePropertyId); + List GetAcquisitionFileExportDeep(AcquisitionFilter filter, HashSet regions, long? contractorPersonId = null); } } diff --git a/source/backend/dal/Repositories/Interfaces/IAgreementRepository.cs b/source/backend/dal/Repositories/Interfaces/IAgreementRepository.cs index e82fb11ee5..9aa8618252 100644 --- a/source/backend/dal/Repositories/Interfaces/IAgreementRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IAgreementRepository.cs @@ -6,7 +6,7 @@ namespace Pims.Dal.Repositories { public interface IAgreementRepository : IRepository { - List GetAgreementsByAquisitionFile(long acquisitionFileId); + List GetAgreementsByAcquisitionFile(long acquisitionFileId); List SearchAgreements(AcquisitionReportFilterModel filter); diff --git a/source/backend/entities/Partials/Agreement.cs b/source/backend/entities/Partials/Agreement.cs index c5e7a1688b..4fdb6c0758 100644 --- a/source/backend/entities/Partials/Agreement.cs +++ b/source/backend/entities/Partials/Agreement.cs @@ -11,5 +11,28 @@ public partial class PimsAgreement : StandardIdentityBaseAppEntity, IBaseA [NotMapped] public override long Internal_Id { get => this.AgreementId; set => this.AgreementId = value; } #endregion + + public bool IsEqual(PimsAgreement other) + { + return AgreementId == other.AgreementId && + AcquisitionFileId == other.AcquisitionFileId && + AgreementTypeCode == other.AgreementTypeCode && + AgreementStatusTypeCode == other.AgreementStatusTypeCode && + AgreementDate == other.AgreementDate && + CompletionDate == other.CompletionDate && + TerminationDate == other.TerminationDate && + CommencementDate == other.CommencementDate && + DepositAmount == other.DepositAmount && + NoLaterThanDays == other.NoLaterThanDays && + PurchasePrice == other.PurchasePrice && + LegalSurveyPlanNum == other.LegalSurveyPlanNum && + OfferDate == other.OfferDate && + ExpiryTs == other.ExpiryTs && + SignedDate == other.SignedDate && + InspectionDate == other.InspectionDate && + ExpropriationDate == other.ExpropriationDate && + PossessionDate == other.PossessionDate && + CancellationNote == other.CancellationNote; + } } } diff --git a/source/backend/tests/core/TestHelper.cs b/source/backend/tests/core/TestHelper.cs index d3d01896aa..c64897e31a 100644 --- a/source/backend/tests/core/TestHelper.cs +++ b/source/backend/tests/core/TestHelper.cs @@ -11,8 +11,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; +using Pims.Api.Services; using Pims.Dal; using Pims.Dal.Configuration.Generators; +using Pims.Dal.Entities; namespace Pims.Core.Test { @@ -236,6 +238,7 @@ public IFormFile GetFormFile(string text) IFormFile file = new FormFile(stream, 0, stream.Length, "id_from_form", fileName); return file; } + #endregion } } diff --git a/source/backend/tests/unit/api/Services/AcquisitionFileServiceTest.cs b/source/backend/tests/unit/api/Services/AcquisitionFileServiceTest.cs index 580f5db49c..ec14f0a231 100644 --- a/source/backend/tests/unit/api/Services/AcquisitionFileServiceTest.cs +++ b/source/backend/tests/unit/api/Services/AcquisitionFileServiceTest.cs @@ -278,7 +278,10 @@ public void Update_Success() userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var result = service.Update(acqFile, new List() { UserOverrideCode.UpdateRegion }); @@ -325,11 +328,13 @@ public void Update_CannotDetermineRegion() var acqFile = EntityHelper.CreateAcquisitionFile(); acqFile.RegionCode = 4; acqFile.ConcurrencyControlNumber = 1; + acqFile.AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString(); var repository = this._helper.GetService>(); repository.Setup(x => x.Update(It.IsAny())).Returns(acqFile); repository.Setup(x => x.GetRowVersion(It.IsAny())).Returns(1); repository.Setup(x => x.GetRegion(It.IsAny())).Returns(acqFile.RegionCode); + repository.Setup(x => x.GetById(It.IsAny())).Returns(acqFile); var compReqRepository = this._helper.GetService>(); compReqRepository.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny())).Returns(new List()); @@ -341,7 +346,10 @@ public void Update_CannotDetermineRegion() userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act Action act = () => service.Update(acqFile, new List() { UserOverrideCode.AddLocationToProperty }); @@ -392,20 +400,25 @@ public void Update_Region_Violation() var service = this.CreateAcquisitionServiceWithPermissions(Permissions.AcquisitionFileEdit); var acqFile = EntityHelper.CreateAcquisitionFile(); + acqFile.AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString(); var repository = this._helper.GetService>(); repository.Setup(x => x.GetRowVersion(It.IsAny())).Returns(1); repository.Setup(x => x.GetRegion(It.IsAny())).Returns((short)(acqFile.RegionCode + 100)); + repository.Setup(x => x.GetById(It.IsAny())).Returns(acqFile); var userRepository = this._helper.GetService>(); userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); var compReqRepository = this._helper.GetService>(); compReqRepository.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny())).Returns(new List()); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); + // Act Action act = () => service.Update(acqFile, new List()); @@ -448,7 +461,7 @@ public void Update_Drafts_Violation() repository.Setup(x => x.GetRowVersion(It.IsAny())).Returns(1); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List() { new PimsAgreement() { AgreementStatusTypeCode = "DRAFT" } }); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List() { new PimsAgreement() { AgreementStatusTypeCode = "DRAFT" } }); var userRepository = this._helper.GetService>(); userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); @@ -483,7 +496,10 @@ public void Update_Success_Region_UserOverride() compReqRepository.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny())).Returns(new List()); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var result = service.Update(acqFile, new List() { UserOverrideCode.UpdateRegion }); @@ -521,7 +537,10 @@ public void Update_PropertyOfInterest_Violation_Owned() takeRepository.Setup(x => x.GetAllByPropertyAcquisitionFileId(It.IsAny())).Returns(new List() { new PimsTake() { IsNewHighwayDedication = true } }); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act Action act = () => service.Update(acqFile, new List() { UserOverrideCode.UpdateRegion }); @@ -559,13 +578,16 @@ public void Update_PropertyOfInterest_Violation_Other() var takeRepository = this._helper.GetService>(); takeRepository.Setup(x => x.GetAllByPropertyAcquisitionFileId(It.IsAny())).Returns(new List() { new PimsTake() { IsNewInterestInSrw = true } }); - + var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); var compReqRepository = this._helper.GetService>(); compReqRepository.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny())).Returns(new List()); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); + // Act Action act = () => service.Update(acqFile, new List() { UserOverrideCode.UpdateRegion }); @@ -611,7 +633,10 @@ public void Update_Success_PropertyOfInterest_UserOverride() compReqRepository.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny())).Returns(new List()); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var result = service.Update(acqFile, new List() { UserOverrideCode.UpdateRegion, UserOverrideCode.PoiToInventory }); @@ -671,7 +696,10 @@ public void Update_Success_Transfer_MultipleTakes_Core(List takes, boo takeRepository.Setup(x => x.GetAllByPropertyAcquisitionFileId(It.IsAny())).Returns(takes); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var result = service.Update(acqFile, new List() { UserOverrideCode.UpdateRegion, UserOverrideCode.PoiToInventory }); @@ -715,7 +743,10 @@ public void Update_Success_AddsNote() userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var result = service.Update(acqFile, new List() { UserOverrideCode.UpdateRegion }); @@ -726,6 +757,41 @@ public void Update_Success_AddsNote() && x.Note.NoteTxt == "Acquisition File status changed from Closed to Active")), Times.Once); } + [Fact] + public void Update_InvalidStatus() + { + // Arrange + var service = this.CreateAcquisitionServiceWithPermissions(Permissions.AcquisitionFileEdit); + + var acqFile = EntityHelper.CreateAcquisitionFile(); + acqFile.ConcurrencyControlNumber = 1; + + var repository = this._helper.GetService>(); + repository.Setup(x => x.GetRowVersion(It.IsAny())).Returns(1); + repository.Setup(x => x.Update(It.IsAny())).Returns(acqFile); + repository.Setup(x => x.GetById(It.IsAny())).Returns(acqFile); + + var compReqRepository = this._helper.GetService>(); + compReqRepository.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny())).Returns(new List()); + + var lookupRepository = this._helper.GetService>(); + lookupRepository.Setup(x => x.GetAllRegions()).Returns(new List() { new PimsRegion() { Code = 4, RegionName = "Cannot determine" } }); + var userRepository = this._helper.GetService>(); + userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); + + var agreementRepository = this._helper.GetService>(); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(false); + + // Act + Action act = () => service.Update(acqFile, new List() { UserOverrideCode.UpdateRegion }); + + // Assert + act.Should().Throw(); + } + [Fact] public void UpdateProperties_Success() { @@ -1091,11 +1157,14 @@ public void Update_DuplicateTeam() acqFile.PimsAcquisitionFileTeams.Add(new PimsAcquisitionFileTeam() { PersonId = 1, AcqFlTeamProfileTypeCode = "test" }); acqFile.PimsAcquisitionFileTeams.Add(new PimsAcquisitionFileTeam() { PersonId = 1, AcqFlTeamProfileTypeCode = "test" }); acqFile.ConcurrencyControlNumber = 1; + acqFile.AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString(); var repository = this._helper.GetService>(); repository.Setup(x => x.Update(It.IsAny())).Returns(acqFile); repository.Setup(x => x.GetRowVersion(It.IsAny())).Returns(1); repository.Setup(x => x.GetRegion(It.IsAny())).Returns(acqFile.RegionCode); + repository.Setup(x => x.GetById(It.IsAny())).Returns(acqFile); + var lookupRepository = this._helper.GetService>(); lookupRepository.Setup(x => x.GetAllRegions()).Returns(new List() { new PimsRegion() { Code = 4, RegionName = "Cannot determine" } }); @@ -1103,11 +1172,14 @@ public void Update_DuplicateTeam() userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); var compReqRepository = this._helper.GetService>(); compReqRepository.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny())).Returns(new List()); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); + // Act Action act = () => service.Update(acqFile, new List() { UserOverrideCode.AddPropertyToInventory }); @@ -1121,7 +1193,7 @@ public void Update_Contractor_Removed() var service = this.CreateAcquisitionServiceWithPermissions(Permissions.AcquisitionFileEdit); var acqFile = EntityHelper.CreateAcquisitionFile(); - acqFile.PimsAcquisitionFileTeams.Add(new PimsAcquisitionFileTeam() { PersonId = 1, AcqFlTeamProfileTypeCode = EnumUserTypeCodes.CONTRACT.ToString() }); + acqFile.PimsAcquisitionFileTeams.Add(new PimsAcquisitionFileTeam() { PersonId = 1, AcqFlTeamProfileTypeCode = EnumUserTypeCodes.CONTRACT.ToString(), }); var repository = this._helper.GetService>(); var userRepository = this._helper.GetService>(); @@ -1134,7 +1206,7 @@ public void Update_Contractor_Removed() repository.Setup(x => x.GetRegion(It.IsAny())).Returns(acqFile.RegionCode); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); var compReqRepository = this._helper.GetService>(); compReqRepository.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny())).Returns(new List()); @@ -1142,6 +1214,9 @@ public void Update_Contractor_Removed() var updatedFile = EntityHelper.CreateAcquisitionFile(); updatedFile.ConcurrencyControlNumber = 1; + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); + // Act Action act = () => service.Update(updatedFile, new List() { UserOverrideCode.AddPropertyToInventory }); @@ -1183,7 +1258,10 @@ public void Update_FKExeption_Removed_AcqFileOwner() userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var updatedAcqFile = EntityHelper.CreateAcquisitionFile(); @@ -1229,7 +1307,10 @@ public void Update_FKExeption_Removed_OwnerSolicitor() userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var updatedAcqFile = EntityHelper.CreateAcquisitionFile(); @@ -1275,7 +1356,10 @@ public void Update_FKExeption_Removed_OwnerRepresentative() userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var updatedAcqFile = EntityHelper.CreateAcquisitionFile(); @@ -1320,7 +1404,10 @@ public void Update_FKExeption_Removed_PersonOfInterest() userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var updatedAcqFile = EntityHelper.CreateAcquisitionFile(); @@ -1364,7 +1451,10 @@ public void Update_NewTotalAllowableCompensation_Success() lookupRepository.Setup(x => x.GetAllRegions()).Returns(new List() { new PimsRegion() { Code = 4, RegionName = "Cannot determine" } }); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var updatedAcqFile = EntityHelper.CreateAcquisitionFile(); @@ -1408,7 +1498,10 @@ public void Update_NewTotalAllowableCompensation_Failure_LessThenCurrentFinancia new List() { new PimsCompReqFinancial() { TotalAmt = 1000 } }); var agreementRepository = this._helper.GetService>(); - agreementRepository.Setup(x => x.GetAgreementsByAquisitionFile(It.IsAny())).Returns(new List()); + agreementRepository.Setup(x => x.GetAgreementsByAcquisitionFile(It.IsAny())).Returns(new List()); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); // Act var updatedAcqFile = EntityHelper.CreateAcquisitionFile(); @@ -1570,13 +1663,16 @@ public void UpdateChecklist_Success() var userRepository = this._helper.GetService>(); userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditChecklists(It.IsAny())).Returns(true); + // Act service.UpdateChecklistItems(acqFile); // Assert fileChecklistRepository.Verify(x => x.GetAllChecklistItemsByAcquisitionFileId(It.IsAny()), Times.Once); fileChecklistRepository.Verify(x => x.Update(It.IsAny()), Times.Once); - repository.Verify(x => x.GetById(It.IsAny()), Times.Exactly(2)); + repository.Verify(x => x.GetById(It.IsAny()), Times.Exactly(3)); } [Fact] @@ -1587,9 +1683,10 @@ public void UpdateChecklist_ItemNotFound() var acqFile = EntityHelper.CreateAcquisitionFile(); acqFile.PimsAcquisitionChecklistItems = new List() { new PimsAcquisitionChecklistItem() { Internal_Id = 999, AcqChklstItemStatusTypeCode = "COMPLT" } }; + acqFile.AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString(); - var repository = this._helper.GetService>(); - repository.Setup(x => x.GetById(It.IsAny())).Returns(acqFile); + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetById(It.IsAny())).Returns(acqFile); var fileChecklistRepository = this._helper.GetService>(); fileChecklistRepository.Setup(x => x.GetAllChecklistItemsByAcquisitionFileId(It.IsAny())) @@ -1598,6 +1695,9 @@ public void UpdateChecklist_ItemNotFound() var userRepository = this._helper.GetService>(); userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditChecklists(It.IsAny())).Returns(true); + // Act Action act = () => service.UpdateChecklistItems(acqFile); @@ -1606,7 +1706,7 @@ public void UpdateChecklist_ItemNotFound() fileChecklistRepository.Verify(x => x.GetAllChecklistItemsByAcquisitionFileId(It.IsAny()), Times.Once); fileChecklistRepository.Verify(x => x.Update(It.IsAny()), Times.Never); - repository.Verify(x => x.GetById(It.IsAny()), Times.Once); + acqRepository.Verify(x => x.GetById(It.IsAny()), Times.Exactly(2)); } [Fact] @@ -1652,6 +1752,35 @@ public void UpdateChecklist_NotAuthorized_Contractor() // Assert act.Should().Throw(); } + + [Fact] + public void UpdateChecklist_InvalidStatus() + { + // Arrange + var service = this.CreateAcquisitionServiceWithPermissions(Permissions.AcquisitionFileEdit); + + var acqFile = EntityHelper.CreateAcquisitionFile(); + acqFile.PimsAcquisitionChecklistItems = new List() { new PimsAcquisitionChecklistItem() { Internal_Id = 1, AcqChklstItemStatusTypeCode = "COMPLT" } }; + + var repository = this._helper.GetService>(); + repository.Setup(x => x.GetById(It.IsAny())).Returns(acqFile); + + var fileChecklistRepository = this._helper.GetService>(); + fileChecklistRepository.Setup(x => x.GetAllChecklistItemsByAcquisitionFileId(It.IsAny())) + .Returns(new List() { new PimsAcquisitionChecklistItem() { Internal_Id = 1, AcqChklstItemStatusTypeCode = "INCOMP" } }); + + var userRepository = this._helper.GetService>(); + userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(EntityHelper.CreateUser("Test")); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditChecklists(It.IsAny())).Returns(false); + + // Act + Action act = () => service.UpdateChecklistItems(acqFile); + + // Assert + act.Should().Throw(); + } #endregion #region CompensationRequisition diff --git a/source/backend/tests/unit/api/Services/CompensationRequisitionServiceTest.cs b/source/backend/tests/unit/api/Services/CompensationRequisitionServiceTest.cs index 851a3b330b..9bfbb6a7a0 100644 --- a/source/backend/tests/unit/api/Services/CompensationRequisitionServiceTest.cs +++ b/source/backend/tests/unit/api/Services/CompensationRequisitionServiceTest.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using FluentAssertions; using Moq; +using Pims.Api.Constants; using Pims.Api.Helpers.Exceptions; using Pims.Api.Services; using Pims.Core.Exceptions; @@ -94,9 +95,14 @@ public void Update_Success_Inserts_StatusChanged_Note() var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit); var noteRepository = this._helper.GetService>(); var compensationRepository = this._helper.GetService>(); - var acqFileRepository = this._helper.GetService>(); - acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() { TotalAllowableCompensation = 100 }); + var acqFileRepository = this._helper.GetService>(); + acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + TotalAllowableCompensation = 100, + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + }); var currentCompensationStub = new PimsCompensationRequisition { @@ -107,7 +113,6 @@ public void Update_Success_Inserts_StatusChanged_Note() }; compensationRepository.Setup(x => x.GetById(It.IsAny())).Returns(currentCompensationStub); - compensationRepository.Setup(x => x.Update(It.IsAny())).Returns(new PimsCompensationRequisition { Internal_Id = 1, @@ -117,6 +122,9 @@ public void Update_Success_Inserts_StatusChanged_Note() FinalizedDate = DateTime.UtcNow, }); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(true); + // Act var result = service.Update( new PimsCompensationRequisition @@ -142,15 +150,23 @@ public void Update_Success_Skips_StatusChanged_Note() var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit); var noteRepository = this._helper.GetService>(); var repository = this._helper.GetService>(); - var acqFileRepository = this._helper.GetService>(); - acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() { TotalAllowableCompensation = 100 }); + var acqFileRepository = this._helper.GetService>(); + acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() + { + TotalAllowableCompensation = 100, + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + }); repository.Setup(x => x.Update(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = true }); repository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = true }); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(true); + + // Act var result = service.Update(new PimsCompensationRequisition() { @@ -174,11 +190,21 @@ public void Update_Status_BackToDraft_NoPermission() // Arrange var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit); var noteRepository = this._helper.GetService>(); - var repository = this._helper.GetService>(); - repository.Setup(x => x.GetById(It.IsAny())) + var compRepository = this._helper.GetService>(); + compRepository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = false }); + var acqFileRepository = this._helper.GetService>(); + acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + }); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(false); + // Act Action act = () => service.Update(new PimsCompensationRequisition() { @@ -189,7 +215,7 @@ public void Update_Status_BackToDraft_NoPermission() }); // Assert - act.Should().Throw(); + act.Should().Throw(); } [Fact] @@ -198,11 +224,21 @@ public void Update_Status_BackToNull_NoPermission() // Arrange var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit); var noteRepository = this._helper.GetService>(); - var repository = this._helper.GetService>(); + var compRepository = this._helper.GetService>(); - repository.Setup(x => x.GetById(It.IsAny())) + compRepository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = false }); + var acqFileRepository = this._helper.GetService>(); + acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + }); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(false); + // Act Action act = () => service.Update(new PimsCompensationRequisition() { @@ -213,7 +249,7 @@ public void Update_Status_BackToNull_NoPermission() }); // Assert - act.Should().Throw(); + act.Should().Throw(); } [Fact] @@ -223,9 +259,13 @@ public void Update_Status_BackToDraft_AuthorizedAdmin() var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit, Permissions.SystemAdmin); var noteRepository = this._helper.GetService>(); var repository = this._helper.GetService>(); - var acqFileRepository = this._helper.GetService>(); - acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() { TotalAllowableCompensation = 100 }); + var acqFileRepository = this._helper.GetService>(); + acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() + { + TotalAllowableCompensation = 100, + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + }); repository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = false }); @@ -233,6 +273,9 @@ public void Update_Status_BackToDraft_AuthorizedAdmin() repository.Setup(x => x.Update(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = true }); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(false); + // Act var result = service.Update(new PimsCompensationRequisition() { @@ -257,9 +300,13 @@ public void Update_Status_BackToNull_AuthorizedAdmin() var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit, Permissions.SystemAdmin); var noteRepository = this._helper.GetService>(); var repository = this._helper.GetService>(); - var acqFileRepository = this._helper.GetService>(); - acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() { TotalAllowableCompensation = 100 }); + var acqFileRepository = this._helper.GetService>(); + acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() + { + TotalAllowableCompensation = 100, + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + }); repository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = false }); @@ -267,6 +314,9 @@ public void Update_Status_BackToNull_AuthorizedAdmin() repository.Setup(x => x.Update(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = null }); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(false); + // Act var result = service.Update(new PimsCompensationRequisition() { @@ -290,16 +340,23 @@ public void Update_Success_Skips_StatusChanged_Note_FromNoStatus() // Arrange var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit); var noteRepository = this._helper.GetService>(); - var repository = this._helper.GetService>(); - var acqFileRepository = this._helper.GetService>(); + var compRepository = this._helper.GetService>(); - acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() { TotalAllowableCompensation = 100 }); + var acqFileRepository = this._helper.GetService>(); + acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() + { + TotalAllowableCompensation = 100, + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + }); - repository.Setup(x => x.Update(It.IsAny())) + compRepository.Setup(x => x.Update(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = true }); ; - repository.Setup(x => x.GetById(It.IsAny())) + compRepository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = null }); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(true); + // Act var result = service.Update(new PimsCompensationRequisition() { @@ -312,7 +369,7 @@ public void Update_Success_Skips_StatusChanged_Note_FromNoStatus() // Assert result.Should().NotBeNull(); result.FinalizedDate.Should().BeNull(); - repository.Verify(x => x.Update(It.IsAny()), Times.Once); + compRepository.Verify(x => x.Update(It.IsAny()), Times.Once); noteRepository.Verify(x => x.Add(It.Is(x => x.AcquisitionFileId == 1 && x.Note.NoteTxt.Equals("Compensation Requisition with # 1, changed status from 'No Status' to 'Draft'"))), Times.Once); } @@ -324,18 +381,25 @@ public void Update_Success_ValidTotalAllowableCompensation() var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit); var compReqH120Service = this._helper.GetService>(); var noteRepository = this._helper.GetService>(); - var repository = this._helper.GetService>(); + var compRepository = this._helper.GetService>(); var acqFileRepository = this._helper.GetService>(); - repository.Setup(x => x.Update(It.IsAny())) + compRepository.Setup(x => x.Update(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = true }); ; - repository.Setup(x => x.GetById(It.IsAny())) + compRepository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = null }); compReqH120Service.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny(), true)).Returns( new List() { new PimsCompReqFinancial() { TotalAmt = 100 } }); - acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() { TotalAllowableCompensation = 100 }); + acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() + { + TotalAllowableCompensation = 100, + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + }); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(true); // Act var result = service.Update(new PimsCompensationRequisition() @@ -349,7 +413,7 @@ public void Update_Success_ValidTotalAllowableCompensation() // Assert result.Should().NotBeNull(); - repository.Verify(x => x.Update(It.IsAny()), Times.Once); + compRepository.Verify(x => x.Update(It.IsAny()), Times.Once); } [Fact] @@ -359,12 +423,12 @@ public void Update_Success_ValidMultipleTotalAllowableCompensation() var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit); var compReqH120Service = this._helper.GetService>(); var noteRepository = this._helper.GetService>(); - var repository = this._helper.GetService>(); + var compRepository = this._helper.GetService>(); var acqFileRepository = this._helper.GetService>(); - repository.Setup(x => x.Update(It.IsAny())) + compRepository.Setup(x => x.Update(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = true }); ; - repository.Setup(x => x.GetById(It.IsAny())) + compRepository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = null }); compReqH120Service.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny(), true)).Returns( @@ -375,8 +439,12 @@ public void Update_Success_ValidMultipleTotalAllowableCompensation() TotalAllowableCompensation = 300, PimsCompensationRequisitions = new List() { new PimsCompensationRequisition() { Internal_Id = 1, PimsCompReqFinancials = new List() { new PimsCompReqFinancial() { TotalAmt = 100 } } }, }, + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(true); + // Act var result = service.Update(new PimsCompensationRequisition() { @@ -389,7 +457,7 @@ public void Update_Success_ValidMultipleTotalAllowableCompensation() // Assert result.Should().NotBeNull(); - repository.Verify(x => x.Update(It.IsAny()), Times.Once); + compRepository.Verify(x => x.Update(It.IsAny()), Times.Once); } [Fact] @@ -399,18 +467,25 @@ public void Update_Success_TotalAllowableExceededDraft() var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit); var compReqH120Service = this._helper.GetService>(); var noteRepository = this._helper.GetService>(); - var repository = this._helper.GetService>(); + var compRepository = this._helper.GetService>(); var acqFileRepository = this._helper.GetService>(); - repository.Setup(x => x.Update(It.IsAny())) + compRepository.Setup(x => x.Update(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = true }); ; - repository.Setup(x => x.GetById(It.IsAny())) + compRepository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = null }); compReqH120Service.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny(), true)).Returns( new List() { new PimsCompReqFinancial() { TotalAmt = 100 } }); - acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() { TotalAllowableCompensation = 100 }); + acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsAcquisitionFile() + { + TotalAllowableCompensation = 100, + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + }); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(true); // Act var result = service.Update(new PimsCompensationRequisition() @@ -424,7 +499,7 @@ public void Update_Success_TotalAllowableExceededDraft() // Assert result.Should().NotBeNull(); - repository.Verify(x => x.Update(It.IsAny()), Times.Once); + compRepository.Verify(x => x.Update(It.IsAny()), Times.Once); } [Fact] @@ -434,12 +509,12 @@ public void Update_Fail_TotalAllowableExceeded() var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit); var compReqH120Service = this._helper.GetService>(); var noteRepository = this._helper.GetService>(); - var repository = this._helper.GetService>(); + var compRepository = this._helper.GetService>(); var acqFileRepository = this._helper.GetService>(); - repository.Setup(x => x.Update(It.IsAny())) + compRepository.Setup(x => x.Update(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = true }); ; - repository.Setup(x => x.GetById(It.IsAny())) + compRepository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = null }); compReqH120Service.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny(), true)).Returns(new List() { }); @@ -449,8 +524,13 @@ public void Update_Fail_TotalAllowableExceeded() TotalAllowableCompensation = 99, PimsCompensationRequisitions = new List() { new PimsCompensationRequisition() { Internal_Id = 1, PimsCompReqFinancials = new List() { new PimsCompReqFinancial() { TotalAmt = 100 } } }, }, + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(true); + + // Act // Assert Action act = () => service.Update(new PimsCompensationRequisition() @@ -471,12 +551,12 @@ public void Update_Fail_ValidMultipleTotalAllowableCompensation() var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionEdit); var compReqH120Service = this._helper.GetService>(); var noteRepository = this._helper.GetService>(); - var repository = this._helper.GetService>(); + var compRepository = this._helper.GetService>(); var acqFileRepository = this._helper.GetService>(); - repository.Setup(x => x.Update(It.IsAny())) + compRepository.Setup(x => x.Update(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = true }); ; - repository.Setup(x => x.GetById(It.IsAny())) + compRepository.Setup(x => x.GetById(It.IsAny())) .Returns(new PimsCompensationRequisition { Internal_Id = 1, AcquisitionFileId = 1, IsDraft = null }); compReqH120Service.Setup(x => x.GetAllByAcquisitionFileId(It.IsAny(), true)).Returns( @@ -488,8 +568,12 @@ public void Update_Fail_ValidMultipleTotalAllowableCompensation() TotalAllowableCompensation = 299, PimsCompensationRequisitions = new List() { new PimsCompensationRequisition() { Internal_Id = 1, PimsCompReqFinancials = new List() { new PimsCompReqFinancial() { TotalAmt = 100 } } }, }, + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(true); + // Act // Assert Action act = () => service.Update(new PimsCompensationRequisition() @@ -521,14 +605,25 @@ public void Delete_Success() { // Arrange var service = this.CreateCompRequisitionServiceWithPermissions(Permissions.CompensationRequisitionDelete); - var repo = this._helper.GetService>(); - repo.Setup(x => x.TryDelete(It.IsAny())); + var compRepository = this._helper.GetService>(); + compRepository.Setup(x => x.TryDelete(It.IsAny())); + compRepository.Setup(x => x.GetById(It.IsAny())).Returns(new PimsCompensationRequisition { Internal_Id = 1 }); + + var acqFileRepository = this._helper.GetService>(); + acqFileRepository.Setup(x => x.GetById(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + }); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditOrDeleteCompensation(It.IsAny(), It.IsAny())).Returns(true); // Act var result = service.DeleteCompensation(1); // Assert - repo.Verify(x => x.TryDelete(It.IsAny()), Times.Once); + compRepository.Verify(x => x.TryDelete(It.IsAny()), Times.Once); } private CompensationRequisitionService CreateCompRequisitionServiceWithPermissions(params Permissions[] permissions) diff --git a/source/backend/tests/unit/api/Services/TakeServiceTest.cs b/source/backend/tests/unit/api/Services/TakeServiceTest.cs index 85e562120c..ff3d059a65 100644 --- a/source/backend/tests/unit/api/Services/TakeServiceTest.cs +++ b/source/backend/tests/unit/api/Services/TakeServiceTest.cs @@ -3,7 +3,9 @@ using System.Diagnostics.CodeAnalysis; using FluentAssertions; using Moq; +using Pims.Api.Constants; using Pims.Api.Services; +using Pims.Core.Exceptions; using Pims.Core.Test; using Pims.Dal.Entities; using Pims.Dal.Exceptions; @@ -123,15 +125,21 @@ public void Update_Success() { // Arrange var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); - var repository = this._helper.GetService>(); - repository.Setup(x => + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => x.UpdateAcquisitionPropertyTakes(It.IsAny(), It.IsAny>())); + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + // Act var result = service.UpdateAcquisitionPropertyTakes(1, new List()); // Assert - repository.Verify(x => x.UpdateAcquisitionPropertyTakes(1, new List()), Times.Once); + takeRepository.Verify(x => x.UpdateAcquisitionPropertyTakes(1, new List()), Times.Once); } [Fact] @@ -146,5 +154,27 @@ public void Update_NoPermission() // Assert act.Should().Throw(); } + + [Fact] + public void Update_InvalidStatus() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => + x.UpdateAcquisitionPropertyTakes(It.IsAny(), It.IsAny>())); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(false); + + // Act + Action act = () => service.UpdateAcquisitionPropertyTakes(1, new List()); + + // Assert + act.Should().Throw(); + } } } diff --git a/source/backend/tests/unit/api/Solvers/AcquisitionStatusSolverTests.cs b/source/backend/tests/unit/api/Solvers/AcquisitionStatusSolverTests.cs new file mode 100644 index 0000000000..373ae4cdc1 --- /dev/null +++ b/source/backend/tests/unit/api/Solvers/AcquisitionStatusSolverTests.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using DocumentFormat.OpenXml.Office2010.Excel; +using FluentAssertions; +using MapsterMapper; +using Microsoft.EntityFrameworkCore; +using Moq; +using NetTopologySuite.Geometries; +using Pims.Api.Constants; +using Pims.Api.Helpers.Exceptions; +using Pims.Api.Models.Concepts; +using Pims.Api.Services; +using Pims.Core.Exceptions; +using Pims.Core.Test; +using Pims.Dal; +using Pims.Dal.Entities; +using Pims.Dal.Entities.Models; +using Pims.Dal.Exceptions; +using Pims.Dal.Repositories; +using Pims.Dal.Security; +using Xunit; + +namespace Pims.Api.Test.Services +{ + [Trait("category", "unit")] + [Trait("category", "api")] + [Trait("group", "acquisition")] + [ExcludeFromCodeCoverage] + public class AcquisitionStatusSolverTests + { + #region Tests + public static IEnumerable CanEditDetailsParameters => + new List + { + new object[] {null, false}, + new object[] {AcquisitionStatusTypes.ACTIVE, true}, + new object[] {AcquisitionStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, false}, + new object[] {AcquisitionStatusTypes.CANCEL, false}, + new object[] {AcquisitionStatusTypes.CLOSED, false}, + new object[] {AcquisitionStatusTypes.COMPLT, false}, + new object[] {AcquisitionStatusTypes.HOLD, false}, + }; + + [Theory] + [MemberData(nameof(CanEditDetailsParameters))] + public void CanEditDetails_Parametrized(AcquisitionStatusTypes? status, bool expectedResult) + { + // Arrange + var solver = new AcquisitionStatusSolver(); + + // Act + var result = solver.CanEditDetails(status); + + // Assert + Assert.Equal(expectedResult, result); + } + + public static IEnumerable CanEditChecklistsParameters => + new List + { + new object[] {null, false}, + new object[] {AcquisitionStatusTypes.ACTIVE, true}, + new object[] {AcquisitionStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, true}, + new object[] {AcquisitionStatusTypes.CANCEL, true}, + new object[] {AcquisitionStatusTypes.CLOSED, true}, + new object[] {AcquisitionStatusTypes.COMPLT, true}, + new object[] {AcquisitionStatusTypes.HOLD, true}, + }; + + [Theory] + [MemberData(nameof(CanEditChecklistsParameters))] + public void CanEditChecklists_Parametrized(AcquisitionStatusTypes? status, bool expectedResult) + { + // Arrange + var solver = new AcquisitionStatusSolver(); + + // Act + var result = solver.CanEditChecklists(status); + + // Assert + Assert.Equal(expectedResult, result); + } + + public static IEnumerable CanEditOrDeleteAgreementParameters => + new List + { + new object[] {null, null, false}, + new object[] {AcquisitionStatusTypes.ACTIVE, null, true}, + new object[] {AcquisitionStatusTypes.ACTIVE, AgreementStatusTypes.CANCELLED, true}, + new object[] {AcquisitionStatusTypes.ACTIVE, AgreementStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.ACTIVE, AgreementStatusTypes.FINAL, true}, + new object[] {AcquisitionStatusTypes.DRAFT, null, true}, + new object[] {AcquisitionStatusTypes.DRAFT, AgreementStatusTypes.CANCELLED, true}, + new object[] {AcquisitionStatusTypes.DRAFT, AgreementStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.DRAFT, AgreementStatusTypes.FINAL, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, null, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, AgreementStatusTypes.CANCELLED, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, AgreementStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, AgreementStatusTypes.FINAL, false}, + new object[] {AcquisitionStatusTypes.CANCEL, null, true}, + new object[] {AcquisitionStatusTypes.CANCEL, AgreementStatusTypes.CANCELLED, true}, + new object[] {AcquisitionStatusTypes.CANCEL, AgreementStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.CANCEL, AgreementStatusTypes.FINAL, false}, + new object[] {AcquisitionStatusTypes.CLOSED, null, true}, + new object[] {AcquisitionStatusTypes.CLOSED, AgreementStatusTypes.CANCELLED, true}, + new object[] {AcquisitionStatusTypes.CLOSED, AgreementStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.CLOSED, AgreementStatusTypes.FINAL, false}, + new object[] {AcquisitionStatusTypes.COMPLT, null, true}, + new object[] {AcquisitionStatusTypes.COMPLT, AgreementStatusTypes.CANCELLED, true}, + new object[] {AcquisitionStatusTypes.COMPLT, AgreementStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.COMPLT, AgreementStatusTypes.FINAL, false}, + new object[] {AcquisitionStatusTypes.HOLD, null, true}, + new object[] {AcquisitionStatusTypes.HOLD, AgreementStatusTypes.CANCELLED, true}, + new object[] {AcquisitionStatusTypes.HOLD, AgreementStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.HOLD, AgreementStatusTypes.FINAL, false}, + }; + + [Theory] + [MemberData(nameof(CanEditOrDeleteAgreementParameters))] + public void CanEditOrDeleteAgreements_Parametrized(AcquisitionStatusTypes? acquisitionStatus, AgreementStatusTypes? agreementStatus, bool expectedResult) + { + // Arrange + var solver = new AcquisitionStatusSolver(); + + // Act + var result = solver.CanEditOrDeleteAgreement(acquisitionStatus, agreementStatus); + + // Assert + Assert.Equal(expectedResult, result); + } + + public static IEnumerable CanEditCompensationsParameters => + new List + { + new object[] {null, null, false}, + new object[] {AcquisitionStatusTypes.ACTIVE, null, true}, + new object[] {AcquisitionStatusTypes.ACTIVE, true, true}, + new object[] {AcquisitionStatusTypes.ACTIVE, false, true}, + new object[] {AcquisitionStatusTypes.DRAFT, null, true}, + new object[] {AcquisitionStatusTypes.DRAFT, true, true}, + new object[] {AcquisitionStatusTypes.DRAFT, false, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, null, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, true, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, false, false}, + new object[] {AcquisitionStatusTypes.CANCEL, null, true}, + new object[] {AcquisitionStatusTypes.CANCEL, true, true}, + new object[] {AcquisitionStatusTypes.CANCEL, false, false}, + new object[] {AcquisitionStatusTypes.CLOSED, null, true}, + new object[] {AcquisitionStatusTypes.CLOSED, true, true}, + new object[] {AcquisitionStatusTypes.CLOSED, false, false}, + new object[] {AcquisitionStatusTypes.COMPLT, null, true}, + new object[] {AcquisitionStatusTypes.COMPLT, true, true}, + new object[] {AcquisitionStatusTypes.COMPLT, false, false}, + new object[] {AcquisitionStatusTypes.HOLD, null, true}, + new object[] {AcquisitionStatusTypes.HOLD, true, true}, + new object[] {AcquisitionStatusTypes.HOLD, false, false}, + }; + + [Theory] + [MemberData(nameof(CanEditCompensationsParameters))] + public void CanEditCompensations_Parametrized(AcquisitionStatusTypes? status, bool? isDraftCompensation, bool expectedResult) + { + // Arrange + var solver = new AcquisitionStatusSolver(); + + // Act + var result = solver.CanEditOrDeleteCompensation(status, isDraftCompensation); + + // Assert + Assert.Equal(expectedResult, result); + } + + public static IEnumerable CanEditTakesParameters => + new List + { + new object[] {null, false}, + new object[] {AcquisitionStatusTypes.ACTIVE, true}, + new object[] {AcquisitionStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, false}, + new object[] {AcquisitionStatusTypes.CANCEL, false}, + new object[] {AcquisitionStatusTypes.CLOSED, false}, + new object[] {AcquisitionStatusTypes.COMPLT, false}, + new object[] {AcquisitionStatusTypes.HOLD, false}, + }; + + [Theory] + [MemberData(nameof(CanEditTakesParameters))] + public void CanEditTakes_Parametrized(AcquisitionStatusTypes? status, bool expectedResult) + { + // Arrange + var solver = new AcquisitionStatusSolver(); + + // Act + var result = solver.CanEditTakes(status); + + // Assert + Assert.Equal(expectedResult, result); + } + + public static IEnumerable CanEditStakeholdersParameters => + new List + { + new object[] {null, false}, + new object[] {AcquisitionStatusTypes.ACTIVE, true}, + new object[] {AcquisitionStatusTypes.DRAFT, true}, + new object[] {AcquisitionStatusTypes.ARCHIV, true}, + new object[] {AcquisitionStatusTypes.CANCEL, true}, + new object[] {AcquisitionStatusTypes.CLOSED, true}, + new object[] {AcquisitionStatusTypes.COMPLT, true}, + new object[] {AcquisitionStatusTypes.HOLD, true}, + }; + + [Theory] + [MemberData(nameof(CanEditStakeholdersParameters))] + public void CanEditStakeholders_Parametrized(AcquisitionStatusTypes? status, bool expectedResult) + { + // Arrange + var solver = new AcquisitionStatusSolver(); + + // Act + var result = solver.CanEditStakeholders(status); + + // Assert + Assert.Equal(expectedResult, result); + } + #endregion + } +} \ No newline at end of file diff --git a/source/frontend/src/constants/acquisitionFileStatus.ts b/source/frontend/src/constants/acquisitionFileStatus.ts new file mode 100644 index 0000000000..25e1847189 --- /dev/null +++ b/source/frontend/src/constants/acquisitionFileStatus.ts @@ -0,0 +1,9 @@ +export enum AcquisitionStatus { + Active = 'ACTIVE', + Archived = 'ARCHIV', + Cancelled = 'CANCEL', + Closed = 'CLOSED', + Complete = 'COMPLT', + Draft = 'DRAFT', + Hold = 'HOLD', +} diff --git a/source/frontend/src/features/acquisition/hooks/useAcquisitionFileExport.ts b/source/frontend/src/features/acquisition/hooks/useAcquisitionFileExport.ts index 95fba6e44e..3e5eb7afd7 100644 --- a/source/frontend/src/features/acquisition/hooks/useAcquisitionFileExport.ts +++ b/source/frontend/src/features/acquisition/hooks/useAcquisitionFileExport.ts @@ -40,7 +40,7 @@ export const useAcquisitionFileExport = () => { if (axios.isAxiosError(axiosError)) { dispatch( logError({ - name: 'GetAquisitionListExport', + name: 'GetAcquisitionListExport', status: axiosError?.response?.status, error: axiosError, }), diff --git a/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.test.tsx b/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.test.tsx index 7d6e630f06..18a17e767c 100644 --- a/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.test.tsx +++ b/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.test.tsx @@ -14,7 +14,7 @@ const setFilter = jest.fn(); // render component under test const setup = (renderOptions: RenderOptions = {}) => { - const utils = render(, { + const utils = render(, { store: { [lookupCodesSlice.name]: { lookupCodes: mockLookups }, }, diff --git a/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.tsx b/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.tsx index 8dc9301e38..152b658aba 100644 --- a/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.tsx +++ b/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.tsx @@ -17,7 +17,7 @@ import { AcquisitionFilterModel, Api_AcquisitionFilter, MultiSelectOption } from export interface IAcquisitionFilterProps { filter?: Api_AcquisitionFilter; setFilter: (filter: Api_AcquisitionFilter) => void; - aquisitionTeam: Api_AcquisitionFileTeam[]; + acquisitionTeam: Api_AcquisitionFileTeam[]; } /** @@ -27,7 +27,7 @@ export interface IAcquisitionFilterProps { export const AcquisitionFilter: React.FC> = ({ filter, setFilter, - aquisitionTeam, + acquisitionTeam, }) => { const onSearchSubmit = ( values: AcquisitionFilterModel, @@ -53,22 +53,22 @@ export const AcquisitionFilter: React.FC mapLookupCode(c)); const acquisitionTeamOptions = useMemo(() => { - if (aquisitionTeam !== undefined) { - return aquisitionTeam?.map(x => ({ + if (acquisitionTeam !== undefined) { + return acquisitionTeam?.map(x => ({ id: x.personId ? `P-${x.personId}` : `O-${x.organizationId}`, text: x.personId ? formatApiPersonNames(x.person) : x.organization?.name ?? '', })); } else { return []; } - }, [aquisitionTeam]); + }, [acquisitionTeam]); return ( enableReinitialize initialValues={ filter - ? AcquisitionFilterModel.fromApi(filter, aquisitionTeam || []) + ? AcquisitionFilterModel.fromApi(filter, acquisitionTeam || []) : new AcquisitionFilterModel() } onSubmit={onSearchSubmit} diff --git a/source/frontend/src/features/acquisition/list/AcquisitionListView.tsx b/source/frontend/src/features/acquisition/list/AcquisitionListView.tsx index 06d55acbd7..696699afa4 100644 --- a/source/frontend/src/features/acquisition/list/AcquisitionListView.tsx +++ b/source/frontend/src/features/acquisition/list/AcquisitionListView.tsx @@ -104,7 +104,7 @@ export const AcquisitionListView: React.FunctionComponent< diff --git a/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/AcquisitionSearchResults.test.tsx b/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/AcquisitionSearchResults.test.tsx index 367ba060c8..dbe10343bb 100644 --- a/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/AcquisitionSearchResults.test.tsx +++ b/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/AcquisitionSearchResults.test.tsx @@ -141,7 +141,7 @@ describe('Acquisition Search Results Table', () => { const { getByText } = setup({ results: [ { - aquisitionTeam: [ + acquisitionTeam: [ { id: 4, acquisitionFileId: 5, diff --git a/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/columns.tsx b/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/columns.tsx index 493916d4dc..f333dda007 100644 --- a/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/columns.tsx +++ b/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/columns.tsx @@ -104,13 +104,13 @@ export const columns: ColumnWithProps[] = [ }, { Header: 'Team member', - accessor: 'aquisitionTeam', + accessor: 'acquisitionTeam', align: 'left', clickable: true, width: 40, maxWidth: 40, Cell: (props: CellProps) => { - const acquisitionTeam = props.row.original.aquisitionTeam; + const acquisitionTeam = props.row.original.acquisitionTeam; const personsInTeam = acquisitionTeam?.filter(x => x.personId !== undefined); const organizationsInTeam = acquisitionTeam?.filter(x => x.organizationId !== undefined); @@ -151,8 +151,8 @@ export const columns: ColumnWithProps[] = [ items={teamAsString ?? []} keyFunction={(item: MemberRoleGroup, index: number) => item.person - ? `aquisition-team-${item.id}-person-${item.person.id ?? index}` - : `aquisition-team-${item.id}-org-${item.organization?.id ?? index}` + ? `acquisition-team-${item.id}-person-${item.person.id ?? index}` + : `acquisition-team-${item.id}-org-${item.organization?.id ?? index}` } renderFunction={(item: MemberRoleGroup) => item.person ? ( diff --git a/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/models.ts b/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/models.ts index 19c72d8deb..e8f69ca6b6 100644 --- a/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/models.ts +++ b/source/frontend/src/features/acquisition/list/AcquisitionSearchResults/models.ts @@ -21,7 +21,7 @@ export class AcquisitionSearchResultModel { fileProperties?: Api_AcquisitionFileProperty[]; project?: Api_Project; alternateProject?: Api_Project; - aquisitionTeam?: Api_AcquisitionFileTeam[]; + acquisitionTeam?: Api_AcquisitionFileTeam[]; compensationRequisitions?: Api_CompensationRequisition[]; static fromApi(base: Api_AcquisitionFile): AcquisitionSearchResultModel { @@ -39,7 +39,7 @@ export class AcquisitionSearchResultModel { newModel.fileProperties = base.fileProperties; newModel.project = base.project; newModel.compensationRequisitions = base.compensationRequisitions; - newModel.aquisitionTeam = base.acquisitionTeam; + newModel.acquisitionTeam = base.acquisitionTeam; return newModel; } } diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/AgreementSubForm.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/AgreementSubForm.tsx index c069523fd1..3de9c8230e 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/AgreementSubForm.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/AgreementSubForm.tsx @@ -26,6 +26,7 @@ export interface IAgreementSubFormProps { nameSpace: string; formikProps: FormikProps; agreementTypes: ILookupCode[]; + isDisabled: boolean; } export const AgreementSubForm: React.FunctionComponent = ({ @@ -33,6 +34,7 @@ export const AgreementSubForm: React.FunctionComponent = nameSpace, formikProps, agreementTypes, + isDisabled, }) => { const H0074Type = 'H0074'; const { getOptionsByType } = useLookupCodeHelpers(); @@ -68,6 +70,7 @@ export const AgreementSubForm: React.FunctionComponent = setDisplayModal(true); } }, [agreement, setFieldValue, nameSpace, setDisplayModal, setModalContent]); + return ( <> Agreement details @@ -75,15 +78,16 @@ export const AgreementSubForm: React.FunctionComponent = + )} - + + days. @@ -154,6 +165,7 @@ export const AgreementSubForm: React.FunctionComponent = diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/UpdateAgreementsContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/UpdateAgreementsContainer.tsx index 50ef0ba13e..815e1f713a 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/UpdateAgreementsContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/UpdateAgreementsContainer.tsx @@ -2,6 +2,7 @@ import { FormikProps } from 'formik'; import React, { useEffect, useState } from 'react'; import * as API from '@/constants/API'; +import { useAcquisitionProvider } from '@/hooks/repositories/useAcquisitionProvider'; import { useAgreementProvider } from '@/hooks/repositories/useAgreementProvider'; import { useLookupCodeHelpers } from '@/hooks/useLookupCodeHelpers'; @@ -27,6 +28,14 @@ export const UpdateAgreementsContainer: React.FC( new AgreementsFormModel(acquisitionFileId), @@ -34,11 +43,12 @@ export const UpdateAgreementsContainer: React.FC { const fetchData = async () => { + getAcquisition(acquisitionFileId); const agreements = (await getAgreements(acquisitionFileId)) || []; setInitialValues(AgreementsFormModel.fromApi(acquisitionFileId, agreements)); }; fetchData(); - }, [acquisitionFileId, getAgreements]); + }, [acquisitionFileId, getAcquisition, getAgreements]); const saveAgreements = async (apiAcquisitionFile: AgreementsFormModel) => { const result = await updateAcquisitionAgreements(acquisitionFileId, apiAcquisitionFile.toApi()); @@ -50,7 +60,8 @@ export const UpdateAgreementsContainer: React.FC x.type === AGREEMENT_TYPES); let mockViewProps: IUpdateAgreementsFormProps = { + acquistionFile: undefined, isLoading: false, formikRef: null as any, initialValues: new AgreementsFormModel(0), @@ -40,6 +48,7 @@ describe('UpdateAgreementsForm component', () => { const formikRef = createRef>(); const utils = render( { const agreements = mockAgreementsResponse(); mockViewProps.initialValues = AgreementsFormModel.fromApi(1, agreements); + (StatusUpdateSolver as jest.Mock).mockImplementation(() => organizerMock); + organizerMock.canEditOrDeleteAgreement.mockReturnValue(true); }); afterEach(() => { @@ -130,4 +141,15 @@ describe('UpdateAgreementsForm component', () => { expect(queryByText(/Cancellation reason/i)).toBeNull(); expect(formikRef.current?.values.agreements[0].cancellationNote).toBe(''); }); + + it('Cannot edit if not allowed', async () => { + organizerMock.canEditOrDeleteAgreement.mockReturnValue(false); + setup(); + + const element: HTMLSelectElement | null = document.querySelector( + `select[name="agreements.0.agreementStatusTypeCode"]`, + ); + + expect(element).toHaveAttribute('disabled'); + }); }); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/UpdateAgreementsForm.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/UpdateAgreementsForm.tsx index 248068dc41..4fcc9395f1 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/UpdateAgreementsForm.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/UpdateAgreementsForm.tsx @@ -7,16 +7,20 @@ import styled from 'styled-components'; import { Button, StyledRemoveLinkButton } from '@/components/common/buttons'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import { Section } from '@/components/common/Section/Section'; +import TooltipIcon from '@/components/common/TooltipIcon'; import { getDeleteModalProps, useModalContext } from '@/hooks/useModalContext'; +import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; import { Api_Agreement } from '@/models/api/Agreement'; import { ILookupCode } from '@/store/slices/lookupCodes'; +import StatusUpdateSolver from '../../fileDetails/detail/statusUpdateSolver'; import AgreementSubForm from './AgreementSubForm'; import { AgreementsFormModel, SingleAgreementFormModel } from './models'; import { UpdateAgreementsYupSchema } from './UpdateAgreementsYupSchema'; export interface IUpdateAgreementsFormProps { isLoading: boolean; + acquistionFile: Api_AcquisitionFile | undefined; formikRef: React.Ref>; initialValues: AgreementsFormModel; agreementTypes: ILookupCode[]; @@ -24,6 +28,7 @@ export interface IUpdateAgreementsFormProps { } export const UpdateAgreementsForm: React.FC = ({ + acquistionFile, isLoading, formikRef, initialValues, @@ -38,6 +43,11 @@ export const UpdateAgreementsForm: React.FC = ({ removeCallback(index); }; + const statusSolver = new StatusUpdateSolver(acquistionFile); + + const cannotEditMessage = + 'The file you are viewing is in a non-editable state. Change the file status to active or draft to allow editing.'; + return ( @@ -75,22 +85,36 @@ export const UpdateAgreementsForm: React.FC = ({ - { - setModalContent({ - ...getDeleteModalProps(), - handleOk: () => { - onRemove(index, arrayHelpers.remove); - setDisplayModal(false); - }, - }); - setDisplayModal(true); - }} - > - - + {!statusSolver.canEditOrDeleteAgreement( + agreement.agreementStatusTypeCode, + ) && ( + + )} + {statusSolver.canEditOrDeleteAgreement( + agreement.agreementStatusTypeCode, + ) && ( + { + setModalContent({ + ...getDeleteModalProps(), + handleOk: () => { + onRemove(index, arrayHelpers.remove); + setDisplayModal(false); + }, + }); + setDisplayModal(true); + }} + > + + + )} = ({ nameSpace={`${field}.${index}`} formikProps={formikProps} agreementTypes={agreementTypes} + isDisabled={ + !statusSolver.canEditOrDeleteAgreement( + agreement.agreementStatusTypeCode, + ) + } /> ))} diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/checklist/detail/AcquisitionChecklistView.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/checklist/detail/AcquisitionChecklistView.tsx index 6962f0f3a7..55caf803b8 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/checklist/detail/AcquisitionChecklistView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/checklist/detail/AcquisitionChecklistView.tsx @@ -20,6 +20,7 @@ import { } from '@/models/api/AcquisitionFile'; import { prettyFormatUTCDate } from '@/utils'; +import StatusUpdateSolver from '../../fileDetails/detail/statusUpdateSolver'; import { StyledChecklistItemStatus, StyledSectionCentered } from './styles'; export interface IAcquisitionChecklistViewProps { @@ -38,10 +39,12 @@ export const AcquisitionChecklistView: React.FC const checklist = acquisitionFile?.acquisitionFileChecklist || []; const lastUpdated = lastModifiedBy(checklist); + const statusSolver = new StatusUpdateSolver(acquisitionFile); + return ( - {keycloak.hasClaim(Claims.ACQUISITION_EDIT) && acquisitionFile !== undefined ? ( + {keycloak.hasClaim(Claims.ACQUISITION_EDIT) && statusSolver.canEditChecklists() ? ( ) : null} diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/CompensationRequisitionDetailContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/CompensationRequisitionDetailContainer.tsx index a7f5980e99..d0d3c5986e 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/CompensationRequisitionDetailContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/CompensationRequisitionDetailContainer.tsx @@ -91,11 +91,10 @@ export const CompensationRequisitionDetailContainer: React.FunctionComponent< return compensation ? ( { // render component under test const component = render( { }); it('Edit Compensation Button not displayed without claims when is in "Draft" status', async () => { + const acquistionFile = mockAcquisitionFileResponse(); + const mockFinalCompensation = getMockApiDefaultCompensation(); + const { queryByTitle } = await setup({ claims: [Claims.COMPENSATION_REQUISITION_VIEW], + props: { + acquisitionFile: { + ...acquistionFile, + fileStatusTypeCode: { id: AcquisitionStatus.Active }, + }, + compensation: { ...mockFinalCompensation, isDraft: true }, + }, }); const editButton = queryByTitle('Edit compensation requisition'); @@ -143,10 +156,17 @@ describe('Compensation Detail View Component', () => { }); it('User does not have the option to Edit Compensation when is in "FINAL" status', async () => { + const acquistionFile = mockAcquisitionFileResponse(); const mockFinalCompensation = getMockApiDefaultCompensation(); const { queryByTitle } = await setup({ claims: [Claims.COMPENSATION_REQUISITION_EDIT], - props: { compensation: { ...mockFinalCompensation, isDraft: false } }, + props: { + acquisitionFile: { + ...acquistionFile, + fileStatusTypeCode: { id: AcquisitionStatus.Complete }, + }, + compensation: { ...mockFinalCompensation, isDraft: false }, + }, }); const editButton = queryByTitle('Edit compensation requisition'); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/CompensationRequisitionDetailView.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/CompensationRequisitionDetailView.tsx index fc78f40861..04b0c4392e 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/CompensationRequisitionDetailView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/CompensationRequisitionDetailView.tsx @@ -11,23 +11,24 @@ import { Section } from '@/components/common/Section/Section'; import { SectionField } from '@/components/common/Section/SectionField'; import { StyledSummarySection } from '@/components/common/Section/SectionStyles'; import { StyledAddButton } from '@/components/common/styles'; +import TooltipIcon from '@/components/common/TooltipIcon'; import { Claims, Roles } from '@/constants'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; +import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; import { Api_CompensationRequisition } from '@/models/api/CompensationRequisition'; import { Api_Organization } from '@/models/api/Organization'; import { Api_Person } from '@/models/api/Person'; -import { Api_Product, Api_Project } from '@/models/api/Project'; import { formatMoney, prettyFormatDate } from '@/utils'; import { formatApiPersonNames } from '@/utils/personUtils'; import { DetailAcquisitionFileOwner } from '../../../models/DetailAcquisitionFileOwner'; +import StatusUpdateSolver from '../../fileDetails/detail/statusUpdateSolver'; export interface CompensationRequisitionDetailViewProps { + acquisitionFile: Api_AcquisitionFile; compensation: Api_CompensationRequisition; compensationContactPerson: Api_Person | undefined; compensationContactOrganization: Api_Organization | undefined; - acqFileProject?: Api_Project; - acqFileProduct?: Api_Product | undefined; clientConstant: string; loading: boolean; setEditMode: (editMode: boolean) => void; @@ -45,11 +46,10 @@ interface PayeeViewDetails { export const CompensationRequisitionDetailView: React.FunctionComponent< React.PropsWithChildren > = ({ + acquisitionFile, compensation, compensationContactPerson, compensationContactOrganization, - acqFileProject, - acqFileProduct, clientConstant, loading, setEditMode, @@ -126,18 +126,27 @@ export const CompensationRequisitionDetailView: React.FunctionComponent< .map(f => f.totalAmount ?? 0) .reduce((prev, next) => prev + next, 0); + const acqFileProject = acquisitionFile?.project; + const acqFileProduct = acquisitionFile?.product; + + const statusSolver = new StatusUpdateSolver(acquisitionFile); + const userCanEditCompensationReq = (): boolean => { - if (compensation.isDraft && hasClaim(Claims.COMPENSATION_REQUISITION_EDIT)) { + if ( + statusSolver.canEditOrDeleteCompensation(compensation.isDraft) && + hasClaim(Claims.COMPENSATION_REQUISITION_EDIT) + ) { return true; - } - - if (!compensation.isDraft && hasRole(Roles.SYSTEM_ADMINISTRATOR)) { + } else if (hasRole(Roles.SYSTEM_ADMINISTRATOR)) { return true; } return false; }; + const cannotEditMessage = + 'The file you are viewing is in a non-editable state. Change the file status to active or draft to allow editing.'; + const editButtonBlock = ( {setEditMode !== undefined && userCanEditCompensationReq() && editButtonBlock} - + {!userCanEditCompensationReq() && ( + + )} { onGenerate(compensation); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/__snapshots__/CompensationRequisitionDetailView.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/__snapshots__/CompensationRequisitionDetailView.test.tsx.snap index 6b34550147..6a50a168d5 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/__snapshots__/CompensationRequisitionDetailView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/detail/__snapshots__/CompensationRequisitionDetailView.test.tsx.snap @@ -381,6 +381,26 @@ exports[`Compensation Detail View Component renders as expected 1`] = `
+ + + + +
onAddCompensationRequisition(fileId)} onDelete={async (compensationId: number) => { @@ -148,7 +149,6 @@ export const CompensationListContainer: React.FunctionComponent< }); setDisplayModal(true); }} - totalAllowableCompensation={file?.totalAllowableCompensation || 0} /> ); }; diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListView.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListView.test.tsx index 6cb2e30e42..74f0814eaf 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListView.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListView.test.tsx @@ -1,12 +1,13 @@ import { createMemoryHistory } from 'history'; +import { AcquisitionStatus } from '@/constants/acquisitionFileStatus'; import Claims from '@/constants/claims'; import { emptyCompensationFinancial, emptyCompensationRequisition, getMockApiCompensationList, } from '@/mocks/compensations.mock'; -import { mockLookups } from '@/mocks/index.mock'; +import { mockAcquisitionFileResponse, mockLookups } from '@/mocks/index.mock'; import { Api_CompensationRequisition } from '@/models/api/CompensationRequisition'; import { lookupCodesSlice } from '@/store/slices/lookupCodes'; import { act, render, RenderOptions, userEvent, waitFor } from '@/utils/test-utils'; @@ -24,11 +25,14 @@ const onDelete = jest.fn(); const onAddCompensationRequisition = jest.fn(); const onUpdateTotalCompensation = jest.fn(); +const mockAcquisitionfile = mockAcquisitionFileResponse(); + describe('compensation list view', () => { const setup = (renderOptions?: RenderOptions & Partial) => { // render component under test const component = render( { }); it('can click the delete action on a given row', async () => { + const compensations = getMockApiCompensationList(); const { findAllByTitle } = setup({ - compensations: getMockApiCompensationList(), + acquisitionFile: { + ...mockAcquisitionFileResponse(), + fileStatusTypeCode: { id: AcquisitionStatus.Active }, + }, + compensations: compensations, claims: [Claims.COMPENSATION_REQUISITION_DELETE], }); const deleteButton = (await findAllByTitle('Delete Compensation'))[0]; act(() => userEvent.click(deleteButton)); await waitFor(() => { - expect(onDelete).toHaveBeenCalledWith(4); + expect(onDelete).toHaveBeenCalledWith(compensations[0].id); }); }); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListView.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListView.tsx index b17c896558..721b7e2d3c 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListView.tsx @@ -9,26 +9,28 @@ import { Section } from '@/components/common/Section/Section'; import { SectionField } from '@/components/common/Section/SectionField'; import { SectionListHeader } from '@/components/common/SectionListHeader'; import Claims from '@/constants/claims'; +import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; import { Api_CompensationFinancial } from '@/models/api/CompensationFinancial'; import { Api_CompensationRequisition } from '@/models/api/CompensationRequisition'; import { formatMoney } from '@/utils'; +import StatusUpdateSolver from '../../fileDetails/detail/statusUpdateSolver'; import { CompensationResults } from './CompensationResults'; export interface ICompensationListViewProps { + acquisitionFile: Api_AcquisitionFile; compensations: Api_CompensationRequisition[]; onAdd: () => void; onDelete: (compensationId: number) => void; onUpdateTotalCompensation: (totalAllowableCompensation: number | null) => Promise; - totalAllowableCompensation?: number; } export const CompensationListView: React.FunctionComponent = ({ + acquisitionFile, compensations, onAdd, onDelete, onUpdateTotalCompensation: onUpdateCompensation, - totalAllowableCompensation, }) => { const history = useHistory(); const match = useRouteMatch(); @@ -59,13 +61,17 @@ export const CompensationListView: React.FunctionComponent } onAdd={onAdd} @@ -127,6 +133,7 @@ export const CompensationListView: React.FunctionComponent { history.push(`${match.url}/compensation-requisition/${compensationId}`); }} diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationResults.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationResults.tsx index 5541c632d4..fc048120c2 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationResults.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationResults.tsx @@ -1,10 +1,12 @@ import { Table } from '@/components/Table'; import { Api_CompensationRequisition } from '@/models/api/CompensationRequisition'; +import StatusUpdateSolver from '../../fileDetails/detail/statusUpdateSolver'; import { createCompensationTableColumns } from './columns'; export interface ICompensationResultProps { results: Api_CompensationRequisition[]; + statusSolver: StatusUpdateSolver; onShow: (compensationId: number) => void; onDelete: (compensationId: number) => void; } @@ -12,7 +14,7 @@ export interface ICompensationResultProps { export function CompensationResults(props: ICompensationResultProps) { const { results, ...rest } = props; - const columns = createCompensationTableColumns(props.onShow, props.onDelete); + const columns = createCompensationTableColumns(props.statusSolver, props.onShow, props.onDelete); return ( diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/__snapshots__/CompensationListView.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/__snapshots__/CompensationListView.test.tsx.snap index 693656f0b8..9d56e470ba 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/__snapshots__/CompensationListView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/__snapshots__/CompensationListView.test.tsx.snap @@ -82,7 +82,7 @@ exports[`compensation list view renders as expected 1`] = `
- Add Compensation + Compensation Requisitions
void, onDelete: (compensationId: number) => void, ) { @@ -102,16 +105,16 @@ export function createCompensationTableColumns( )} {hasClaim(Claims.COMPENSATION_REQUISITION_DELETE) && - cellProps.row.original.isDraft !== false ? ( - cellProps.row.original.id && onDelete(cellProps.row.original.id)} - title="Delete Compensation" - > - - - ) : null} + statusSolver.canEditOrDeleteCompensation(cellProps.row.original.isDraft) && ( + cellProps.row.original.id && onDelete(cellProps.row.original.id)} + title="Delete Compensation" + > + + + )} ); }, diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.tsx index 165eced9a1..d08069a033 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.tsx @@ -46,8 +46,8 @@ export const UpdateForm8Container: React.FunctionComponent< }, } = useInterestHolderRepository(); - const aquisitionPath = location.pathname.split(`/${FileTabType.EXPROPRIATION}/${form8Id}`)[0]; - const backUrl = `${aquisitionPath}/${FileTabType.EXPROPRIATION}`; + const acquisitionPath = location.pathname.split(`/${FileTabType.EXPROPRIATION}/${form8Id}`)[0]; + const backUrl = `${acquisitionPath}/${FileTabType.EXPROPRIATION}`; const loadForm8Details = useCallback(async () => { const form8Api = await getForm8(form8Id); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.tsx index f645dfaa81..fddc1a8852 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.tsx @@ -7,7 +7,8 @@ import EditButton from '@/components/common/EditButton'; import { Section } from '@/components/common/Section/Section'; import { SectionField } from '@/components/common/Section/SectionField'; import { StyledEditWrapper, StyledSummarySection } from '@/components/common/Section/SectionStyles'; -import { Claims } from '@/constants'; +import TooltipIcon from '@/components/common/TooltipIcon'; +import { Claims, Roles } from '@/constants'; import { InterestHolderType } from '@/constants/interestHolderTypes'; import { usePersonRepository } from '@/features/contacts/repositories/usePersonRepository'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; @@ -18,6 +19,7 @@ import { formatApiPersonNames } from '@/utils/personUtils'; import AcquisitionOwnersSummaryContainer from './AcquisitionOwnersSummaryContainer'; import AcquisitionOwnersSummaryView from './AcquisitionOwnersSummaryView'; import { DetailAcquisitionFile } from './models'; +import StatusUpdateSolver from './statusUpdateSolver'; export interface IAcquisitionSummaryViewProps { acquisitionFile?: Api_AcquisitionFile; @@ -31,6 +33,8 @@ const AcquisitionSummaryView: React.FC = ({ const keycloak = useKeycloakWrapper(); const detail: DetailAcquisitionFile = DetailAcquisitionFile.fromApi(acquisitionFile); + const { hasRole } = useKeycloakWrapper(); + const projectName = acquisitionFile?.project !== undefined ? acquisitionFile?.project?.code + ' - ' + acquisitionFile?.project?.description @@ -59,13 +63,32 @@ const AcquisitionSummaryView: React.FC = ({ x => x.interestHolderType?.id === InterestHolderType.OWNER_REPRESENTATIVE, ); + const statusSolver = new StatusUpdateSolver(acquisitionFile); + + const cannotEditMessage = + 'The file you are viewing is in a non-editable state. Change the file status to active or draft to allow editing.'; + + const canEditDetails = () => { + if (hasRole(Roles.SYSTEM_ADMINISTRATOR) || statusSolver.canEditDetails()) { + return true; + } + return false; + }; + return ( - {keycloak.hasClaim(Claims.ACQUISITION_EDIT) && acquisitionFile !== undefined ? ( + {keycloak.hasClaim(Claims.ACQUISITION_EDIT) && canEditDetails() ? ( ) : null} + {!canEditDetails() && ( + + )} +
{projectName} {productName} diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/statusUpdateSolver.ts b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/statusUpdateSolver.ts new file mode 100644 index 0000000000..45bc7ac30c --- /dev/null +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/statusUpdateSolver.ts @@ -0,0 +1,245 @@ +import { AcquisitionStatus } from '@/constants/acquisitionFileStatus'; +import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; +import { AgreementStatusTypes } from '@/models/api/Agreement'; + +class StatusUpdateSolver { + private readonly acquisitionFile: Api_AcquisitionFile | null; + + constructor(apiModel: Api_AcquisitionFile | undefined | null) { + this.acquisitionFile = apiModel ?? null; + } + + canEditDetails(): boolean { + if (this.acquisitionFile === null) { + return false; + } + + const statusCode = this.acquisitionFile.fileStatusTypeCode?.id; + let canEdit = false; + + switch (statusCode) { + case AcquisitionStatus.Active: + case AcquisitionStatus.Draft: + canEdit = true; + break; + case AcquisitionStatus.Archived: + case AcquisitionStatus.Cancelled: + case AcquisitionStatus.Closed: + case AcquisitionStatus.Complete: + case AcquisitionStatus.Hold: + canEdit = false; + break; + default: + canEdit = false; + break; + } + + return canEdit; + } + + canEditTakes(): boolean { + if (this.acquisitionFile === null) { + return false; + } + + const statusCode = this.acquisitionFile.fileStatusTypeCode?.id; + let canEdit = false; + + switch (statusCode) { + case AcquisitionStatus.Active: + case AcquisitionStatus.Draft: + canEdit = true; + break; + case AcquisitionStatus.Archived: + case AcquisitionStatus.Cancelled: + case AcquisitionStatus.Closed: + case AcquisitionStatus.Complete: + case AcquisitionStatus.Hold: + canEdit = false; + break; + default: + canEdit = false; + break; + } + + return canEdit; + } + + canEditOrDeleteCompensation(isDraftCompensation: boolean | null): boolean { + if (this.acquisitionFile === null) { + return false; + } + + const statusCode = this.acquisitionFile.fileStatusTypeCode?.id; + let canEdit = false; + + switch (statusCode) { + case AcquisitionStatus.Active: + case AcquisitionStatus.Draft: + canEdit = true; + break; + case AcquisitionStatus.Archived: + case AcquisitionStatus.Cancelled: + case AcquisitionStatus.Closed: + case AcquisitionStatus.Complete: + case AcquisitionStatus.Hold: + canEdit = isDraftCompensation ?? true; + break; + default: + canEdit = false; + break; + } + + return canEdit; + } + + canEditOrDeleteAgreement(agreementStatusCode: string | null): boolean { + if (this.acquisitionFile === null) { + return false; + } + + const statusCode = this.acquisitionFile.fileStatusTypeCode?.id; + let canEdit = false; + + switch (statusCode) { + case AcquisitionStatus.Active: + case AcquisitionStatus.Draft: + canEdit = true; + break; + case AcquisitionStatus.Archived: + case AcquisitionStatus.Cancelled: + case AcquisitionStatus.Closed: + case AcquisitionStatus.Complete: + case AcquisitionStatus.Hold: + canEdit = agreementStatusCode !== AgreementStatusTypes.FINAL ?? true; + break; + default: + canEdit = false; + break; + } + + return canEdit; + } + + canEditDocuments(): boolean { + if (this.acquisitionFile === null) { + return false; + } + + const statusCode = this.acquisitionFile.fileStatusTypeCode?.id; + let canEdit = false; + + switch (statusCode) { + case AcquisitionStatus.Active: + case AcquisitionStatus.Draft: + case AcquisitionStatus.Archived: + case AcquisitionStatus.Cancelled: + case AcquisitionStatus.Closed: + case AcquisitionStatus.Complete: + case AcquisitionStatus.Hold: + default: + canEdit = true; + break; + } + + return canEdit; + } + + canEditNotes(): boolean { + if (this.acquisitionFile === null) { + return false; + } + + const statusCode = this.acquisitionFile.fileStatusTypeCode?.id; + let canEdit = false; + + switch (statusCode) { + case AcquisitionStatus.Active: + case AcquisitionStatus.Draft: + case AcquisitionStatus.Archived: + case AcquisitionStatus.Cancelled: + case AcquisitionStatus.Closed: + case AcquisitionStatus.Complete: + case AcquisitionStatus.Hold: + default: + canEdit = true; + break; + } + + return canEdit; + } + + canEditChecklists(): boolean { + if (this.acquisitionFile === null) { + return false; + } + + const statusCode = this.acquisitionFile.fileStatusTypeCode?.id; + let canEdit = false; + + switch (statusCode) { + case AcquisitionStatus.Active: + case AcquisitionStatus.Draft: + case AcquisitionStatus.Archived: + case AcquisitionStatus.Cancelled: + case AcquisitionStatus.Closed: + case AcquisitionStatus.Complete: + case AcquisitionStatus.Hold: + default: + canEdit = true; + break; + } + + return canEdit; + } + + canEditStakeholders(): boolean { + if (this.acquisitionFile === null) { + return false; + } + + const statusCode = this.acquisitionFile.fileStatusTypeCode?.id; + let canEdit = false; + + switch (statusCode) { + case AcquisitionStatus.Active: + case AcquisitionStatus.Draft: + case AcquisitionStatus.Archived: + case AcquisitionStatus.Cancelled: + case AcquisitionStatus.Closed: + case AcquisitionStatus.Complete: + case AcquisitionStatus.Hold: + default: + canEdit = true; + break; + } + + return canEdit; + } + + canEditProperties(): boolean { + if (this.acquisitionFile === null) { + return false; + } + + const statusCode = this.acquisitionFile.fileStatusTypeCode?.id; + let canEdit = false; + + switch (statusCode) { + case AcquisitionStatus.Active: + case AcquisitionStatus.Draft: + case AcquisitionStatus.Archived: + case AcquisitionStatus.Cancelled: + case AcquisitionStatus.Closed: + case AcquisitionStatus.Complete: + case AcquisitionStatus.Hold: + default: + canEdit = true; + break; + } + + return canEdit; + } +} + +export default StatusUpdateSolver; diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderContainer.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderContainer.test.tsx index 7afbe5254e..afbae117c8 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderContainer.test.tsx @@ -2,14 +2,12 @@ import { FormikProps } from 'formik'; import { createMemoryHistory } from 'history'; import { forwardRef } from 'react'; -import { InterestHolderType } from '@/constants/interestHolderTypes'; import { mockAcquisitionFileResponse } from '@/mocks/acquisitionFiles.mock'; -import { emptyApiInterestHolder, emptyInterestHolderProperty } from '@/mocks/interestHolder.mock'; import { getMockApiInterestHolders } from '@/mocks/interestHolders.mock'; import { mockLookups } from '@/mocks/lookups.mock'; import { Api_InterestHolder } from '@/models/api/InterestHolder'; import { lookupCodesSlice } from '@/store/slices/lookupCodes'; -import { render, RenderOptions, waitFor } from '@/utils/test-utils'; +import { render, RenderOptions } from '@/utils/test-utils'; import StakeHolderContainer, { IStakeHolderContainerProps } from './StakeHolderContainer'; import { IStakeHolderViewProps } from './StakeHolderView'; @@ -44,10 +42,7 @@ jest.mock('@/hooks/repositories/useInterestHolderRepository', () => ({ describe('StakeHolderContainer component', () => { // render component under test - - let viewProps: IStakeHolderViewProps; const View = forwardRef, IStakeHolderViewProps>((props, ref) => { - viewProps = props; return <>; }); @@ -83,101 +78,4 @@ describe('StakeHolderContainer component', () => { const { asFragment } = setup({}); expect(asFragment()).toMatchSnapshot(); }); - - it('groups multiple interest holder properties by acquisition file id', async () => { - mockGetApi.response = getMockApiInterestHolders(); - setup({}); - await waitFor(async () => { - expect(viewProps.groupedInterestProperties).toHaveLength(2); - expect(viewProps.groupedInterestProperties[0].groupedPropertyInterests).toHaveLength(2); - }); - }); - - it('does not group interest and non-interests for the same property', async () => { - mockGetApi.response = [ - { - ...emptyApiInterestHolder, - interestHolderProperties: [ - { - ...emptyInterestHolderProperty, - propertyInterestTypes: [{ id: 'NIP' }], - acquisitionFilePropertyId: 1, - }, - ], - }, - { - ...emptyApiInterestHolder, - interestHolderProperties: [ - { - ...emptyInterestHolderProperty, - propertyInterestTypes: [{ id: 'IP' }], - acquisitionFilePropertyId: 1, - }, - ], - }, - ]; - setup({}); - await waitFor(async () => { - expect(viewProps.groupedInterestProperties).toHaveLength(1); - expect(viewProps.groupedNonInterestProperties).toHaveLength(1); - }); - }); - - it('does not group interest holders for different properties interest types', async () => { - mockGetApi.response = [ - { - ...emptyApiInterestHolder, - personId: 1, - interestHolderType: { id: InterestHolderType.INTEREST_HOLDER }, - interestHolderProperties: [ - { - ...emptyInterestHolderProperty, - acquisitionFilePropertyId: 1, - propertyInterestTypes: [{ id: 'test_interest_1' }], - }, - ], - }, - { - ...emptyApiInterestHolder, - personId: 1, - interestHolderType: { id: InterestHolderType.INTEREST_HOLDER }, - interestHolderProperties: [ - { - ...emptyInterestHolderProperty, - acquisitionFilePropertyId: 2, - propertyInterestTypes: [{ id: 'test_interest_2' }], - }, - ], - }, - ]; - setup({}); - await waitFor(async () => { - expect(viewProps.groupedInterestProperties).toHaveLength(2); - expect(viewProps.groupedInterestProperties[0].groupedPropertyInterests).toHaveLength(1); - }); - }); - - it('it separates non-interest and interest payees even if they are for the same interest holder property', async () => { - mockGetApi.response = [ - { - ...emptyApiInterestHolder, - personId: 1, - interestHolderType: { id: InterestHolderType.INTEREST_HOLDER }, - interestHolderProperties: [ - { - ...emptyInterestHolderProperty, - acquisitionFilePropertyId: 1, - propertyInterestTypes: [{ id: 'test_interest_1' }, { id: 'NIP' }], - }, - ], - }, - ]; - setup({}); - await waitFor(async () => { - expect(viewProps.groupedInterestProperties).toHaveLength(1); - expect(viewProps.groupedInterestProperties[0].groupedPropertyInterests).toHaveLength(1); - expect(viewProps.groupedNonInterestProperties).toHaveLength(1); - expect(viewProps.groupedNonInterestProperties[0].groupedPropertyInterests).toHaveLength(1); - }); - }); }); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderContainer.tsx index a224ea64a9..cfc628da6f 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderContainer.tsx @@ -3,10 +3,7 @@ import { useEffect } from 'react'; import { useInterestHolderRepository } from '@/hooks/repositories/useInterestHolderRepository'; import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; -import { Api_InterestHolder, Api_InterestHolderProperty } from '@/models/api/InterestHolder'; -import Api_TypeCode from '@/models/api/TypeCode'; -import { InterestHolderViewForm, InterestHolderViewRow } from '../update/models'; import { IStakeHolderViewProps } from './StakeHolderView'; export interface IStakeHolderContainerProps { @@ -34,75 +31,14 @@ export const StakeHolderContainer: React.FunctionComponent interestHolder.interestHolderProperties) ?? []; - const interestProperties = allInterestProperties - .filter(ip => ip.propertyInterestTypes.some(pit => pit?.id !== 'NIP')) - .map(ip => { - const filteredTypes = ip.propertyInterestTypes.filter(pit => pit?.id !== 'NIP'); - return { ...ip, propertyInterestTypes: filteredTypes }; - }); - const nonInterestProperties = allInterestProperties - .filter(ip => ip.propertyInterestTypes.some(pit => pit?.id === 'NIP')) - .map(ip => { - const filteredTypes = ip.propertyInterestTypes.filter(pit => pit?.id === 'NIP'); - return { ...ip, propertyInterestTypes: filteredTypes }; - }); - return ( ); }; -const getGroupedInterestProperties = ( - interestProperties: Api_InterestHolderProperty[], - apiInterestHolders: Api_InterestHolder[], - acquisitionFile: Api_AcquisitionFile, -) => { - const groupedInterestProperties: InterestHolderViewForm[] = []; - interestProperties.forEach((interestHolderProperty: Api_InterestHolderProperty) => { - const matchingGroup = groupedInterestProperties.find( - gip => gip.id === interestHolderProperty.acquisitionFilePropertyId, - ); - const matchingFileProperty = acquisitionFile.fileProperties?.find( - fp => fp.id === interestHolderProperty.acquisitionFilePropertyId, - ); - if (matchingFileProperty && interestHolderProperty) { - interestHolderProperty.acquisitionFileProperty = matchingFileProperty; - } - const interestHolder = apiInterestHolders?.find( - ih => ih.interestHolderId === interestHolderProperty.interestHolderId, - ); - if (!matchingGroup) { - const newGroup = InterestHolderViewForm.fromApi(interestHolderProperty); - newGroup.groupedPropertyInterests = interestHolderProperty.propertyInterestTypes.map( - (itc: Api_TypeCode) => - InterestHolderViewRow.fromApi(interestHolderProperty, interestHolder, itc), - ); - groupedInterestProperties.push(newGroup); - } else { - interestHolderProperty.propertyInterestTypes.forEach((itc: Api_TypeCode) => - matchingGroup.groupedPropertyInterests.push( - InterestHolderViewRow.fromApi(interestHolderProperty, interestHolder, itc), - ), - ); - } - }); - return groupedInterestProperties; -}; - export default StakeHolderContainer; diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderView.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderView.test.tsx index d54356cd5c..f62d1e8d45 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderView.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderView.test.tsx @@ -1,11 +1,13 @@ import { createMemoryHistory } from 'history'; +import { mockAcquisitionFileResponse } from '@/mocks/acquisitionFiles.mock'; import { getMockApiInterestHolders } from '@/mocks/interestHolders.mock'; import { mockLookups } from '@/mocks/lookups.mock'; import { lookupCodesSlice } from '@/store/slices/lookupCodes'; import { render, RenderOptions } from '@/utils/test-utils'; import { InterestHolderViewForm, InterestHolderViewRow } from '../update/models'; +import StakeholderOrganizer from './stakeholderOrganizer'; import StakeHolderView, { IStakeHolderViewProps } from './StakeHolderView'; jest.mock('@react-keycloak/web'); @@ -15,6 +17,13 @@ const storeState = { [lookupCodesSlice.name]: { lookupCodes: mockLookups }, }; +jest.mock('./stakeholderOrganizer'); + +export const organizerMock = { + getInterestProperties: jest.fn(), + getNonInterestProperties: jest.fn(), +}; + const onEdit = jest.fn(); describe('StakeHolderView component', () => { @@ -23,19 +32,8 @@ describe('StakeHolderView component', () => { const utils = render( i.interestHolderProperties) - .map(i => InterestHolderViewForm.fromApi(i)) - } - legacyStakeHolders={renderOptions.props?.legacyStakeHolders ?? []} - groupedNonInterestProperties={ - renderOptions.props?.groupedNonInterestProperties ?? - getMockApiInterestHolders() - .flatMap(i => i.interestHolderProperties) - .map(i => InterestHolderViewForm.fromApi(i)) - } + acquisitionFile={renderOptions.props?.acquisitionFile ?? mockAcquisitionFileResponse()} + interestHolders={renderOptions.props?.interestHolders ?? getMockApiInterestHolders()} loading={renderOptions.props?.loading ?? false} onEdit={onEdit} />, @@ -52,6 +50,22 @@ describe('StakeHolderView component', () => { }; }; + beforeEach(() => { + jest.resetAllMocks(); + + const groupedInterestProperties = getMockApiInterestHolders() + .flatMap(i => i.interestHolderProperties) + .map(i => InterestHolderViewForm.fromApi(i)); + + const groupedNonInterestProperties = getMockApiInterestHolders() + .flatMap(i => i.interestHolderProperties) + .map(i => InterestHolderViewForm.fromApi(i)); + + organizerMock.getInterestProperties.mockReturnValue(groupedInterestProperties); + organizerMock.getNonInterestProperties.mockReturnValue(groupedNonInterestProperties); + (StakeholderOrganizer as jest.Mock).mockImplementation(() => organizerMock); + }); + afterEach(() => { jest.clearAllMocks(); }); @@ -68,8 +82,11 @@ describe('StakeHolderView component', () => { }); it('displays empty warning messages when no values passed', () => { + organizerMock.getInterestProperties.mockReturnValue([]); + organizerMock.getNonInterestProperties.mockReturnValue([]); + const { getByText } = setup({ - props: { loading: true, groupedInterestProperties: [], groupedNonInterestProperties: [] }, + props: { loading: true, acquisitionFile: undefined, interestHolders: [] }, }); expect(getByText('There are no interest holders associated with this file.')).toBeVisible(); @@ -81,12 +98,11 @@ describe('StakeHolderView component', () => { getMockApiInterestHolders()[0].interestHolderProperties[0], ); + organizerMock.getInterestProperties.mockReturnValue([model]); + model.identifier = 'PID: 025-196-375'; const { getByText } = setup({ - props: { - groupedInterestProperties: [model], - groupedNonInterestProperties: [], - }, + props: {}, }); expect(getByText('PID: 025-196-375')).toBeVisible(); @@ -111,11 +127,11 @@ describe('StakeHolderView component', () => { description: 'Registered', }), ]; + + organizerMock.getInterestProperties.mockReturnValue([model]); + const { getByText } = setup({ - props: { - groupedInterestProperties: [model], - groupedNonInterestProperties: [], - }, + props: {}, }); expect(getByText('PID: 025-196-375')).toBeVisible(); @@ -128,12 +144,11 @@ describe('StakeHolderView component', () => { getMockApiInterestHolders()[0].interestHolderProperties[0], ); + organizerMock.getInterestProperties.mockReturnValue([model]); + model.identifier = 'PID: 025-196-375'; const { getByText } = setup({ - props: { - groupedInterestProperties: [], - groupedNonInterestProperties: [model], - }, + props: {}, }); expect(getByText('PID: 025-196-375')).toBeVisible(); @@ -146,7 +161,14 @@ describe('StakeHolderView component', () => { }); it('it displays the legacy stakeholders', () => { - const { queryByTestId } = setup({ props: { legacyStakeHolders: ['John,Doe'] } }); + const { queryByTestId } = setup({ + props: { + acquisitionFile: { + ...mockAcquisitionFileResponse(), + legacyStakeholders: ['John,Doe'], + }, + }, + }); expect(queryByTestId('acq-file-legacy-stakeholders')).toBeInTheDocument(); expect(queryByTestId('acq-file-legacy-stakeholders')).toHaveTextContent('John,Doe'); }); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderView.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderView.tsx index 6b4c8ca4ce..deb7434d18 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/StakeHolderView.tsx @@ -8,26 +8,37 @@ import { StyledEditWrapper, StyledSummarySection } from '@/components/common/Sec import { Claims } from '@/constants/index'; import { StyledNoData } from '@/features/documents/commonStyles'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; +import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; +import { Api_InterestHolder } from '@/models/api/InterestHolder'; -import { InterestHolderViewForm } from '../update/models'; +import StatusUpdateSolver from '../../fileDetails/detail/statusUpdateSolver'; import PropertyInterestHoldersViewTable from './PropertyInterestHoldersViewTable'; +import StakeholderOrganizer from './stakeholderOrganizer'; export interface IStakeHolderViewProps { loading: boolean; - groupedInterestProperties: InterestHolderViewForm[]; - groupedNonInterestProperties: InterestHolderViewForm[]; - legacyStakeHolders: string[]; + acquisitionFile: Api_AcquisitionFile; + interestHolders: Api_InterestHolder[] | undefined; onEdit: () => void; } export const StakeHolderView: React.FunctionComponent = ({ loading, - groupedInterestProperties, - groupedNonInterestProperties, - legacyStakeHolders, + acquisitionFile, + interestHolders, onEdit, }) => { const keycloak = useKeycloakWrapper(); + + const legacyStakeHolders = acquisitionFile.legacyStakeholders ?? []; + + const organizer = new StakeholderOrganizer(acquisitionFile, interestHolders); + + const statusSolver = new StatusUpdateSolver(acquisitionFile); + + const groupedInterestProperties = organizer.getInterestProperties(); + const groupedNonInterestProperties = organizer.getNonInterestProperties(); + return ( <> @@ -35,7 +46,7 @@ export const StakeHolderView: React.FunctionComponent = (
- {keycloak.hasClaim(Claims.ACQUISITION_EDIT) ? ( + {keycloak.hasClaim(Claims.ACQUISITION_EDIT) && statusSolver.canEditStakeholders() ? ( ) : null} diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/stakeholderOrganizer.test.ts b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/stakeholderOrganizer.test.ts new file mode 100644 index 0000000000..edc9de2b38 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/stakeholderOrganizer.test.ts @@ -0,0 +1,130 @@ +import { waitFor } from '@testing-library/react'; + +import { InterestHolderType } from '@/constants/interestHolderTypes'; +import { mockAcquisitionFileResponse } from '@/mocks/acquisitionFiles.mock'; +import { emptyApiInterestHolder, emptyInterestHolderProperty } from '@/mocks/interestHolder.mock'; +import { getMockApiInterestHolders } from '@/mocks/interestHolders.mock'; +import { Api_InterestHolder } from '@/models/api/InterestHolder'; + +import StakeholderOrganizer from './stakeholderOrganizer'; + +describe('StakeholderOrganizer', () => { + it('groups multiple interest holder properties by acquisition file id', async () => { + const acquisitionFile = mockAcquisitionFileResponse(); + const interestHolders = getMockApiInterestHolders(); + + const organizer = new StakeholderOrganizer(acquisitionFile, interestHolders); + + const groupedProperties = organizer.getInterestProperties(); + + expect(groupedProperties).toHaveLength(2); + expect(groupedProperties[0].groupedPropertyInterests).toHaveLength(2); + }); + + it('does not group interest and non-interests for the same property', async () => { + const acquisitionFile = mockAcquisitionFileResponse(); + + const testInterestHolders: Api_InterestHolder[] = [ + { + ...emptyApiInterestHolder, + interestHolderProperties: [ + { + ...emptyInterestHolderProperty, + propertyInterestTypes: [{ id: 'NIP' }], + acquisitionFilePropertyId: 1, + }, + ], + }, + { + ...emptyApiInterestHolder, + interestHolderProperties: [ + { + ...emptyInterestHolderProperty, + propertyInterestTypes: [{ id: 'IP' }], + acquisitionFilePropertyId: 1, + }, + ], + }, + ]; + + const organizer = new StakeholderOrganizer(acquisitionFile, testInterestHolders); + + const interestProperties = organizer.getInterestProperties(); + const nonInterestProperties = organizer.getInterestProperties(); + + await waitFor(async () => { + expect(interestProperties).toHaveLength(1); + expect(nonInterestProperties).toHaveLength(1); + }); + }); + + it('does not group interest holders for different properties interest types', async () => { + const acquisitionFile = mockAcquisitionFileResponse(); + + const testInterestHolders: Api_InterestHolder[] = [ + { + ...emptyApiInterestHolder, + personId: 1, + interestHolderType: { id: InterestHolderType.INTEREST_HOLDER }, + interestHolderProperties: [ + { + ...emptyInterestHolderProperty, + acquisitionFilePropertyId: 1, + propertyInterestTypes: [{ id: 'test_interest_1' }], + }, + ], + }, + { + ...emptyApiInterestHolder, + personId: 1, + interestHolderType: { id: InterestHolderType.INTEREST_HOLDER }, + interestHolderProperties: [ + { + ...emptyInterestHolderProperty, + acquisitionFilePropertyId: 2, + propertyInterestTypes: [{ id: 'test_interest_2' }], + }, + ], + }, + ]; + + const organizer = new StakeholderOrganizer(acquisitionFile, testInterestHolders); + + const interestProperties = organizer.getInterestProperties(); + const nonInterestProperties = organizer.getInterestProperties(); + + expect(interestProperties).toHaveLength(2); + expect(nonInterestProperties[0].groupedPropertyInterests).toHaveLength(1); + }); + + it('it separates non-interest and interest payees even if they are for the same interest holder property', async () => { + const acquisitionFile = mockAcquisitionFileResponse(); + + const testInterestHolders: Api_InterestHolder[] = [ + { + ...emptyApiInterestHolder, + personId: 1, + interestHolderType: { id: InterestHolderType.INTEREST_HOLDER }, + interestHolderProperties: [ + { + ...emptyInterestHolderProperty, + acquisitionFilePropertyId: 1, + propertyInterestTypes: [{ id: 'test_interest_1' }, { id: 'NIP' }], + }, + ], + }, + ]; + + const organizer = new StakeholderOrganizer(acquisitionFile, testInterestHolders); + + const interestProperties = organizer.getInterestProperties(); + const nonInterestProperties = organizer.getInterestProperties(); + + await waitFor(async () => { + expect(interestProperties).toHaveLength(1); + expect(interestProperties[0].groupedPropertyInterests).toHaveLength(1); + expect(nonInterestProperties).toHaveLength(1); + expect(nonInterestProperties[0].groupedPropertyInterests).toHaveLength(1); + }); + }); +}); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/stakeholderOrganizer.ts b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/stakeholderOrganizer.ts new file mode 100644 index 0000000000..b995c39a79 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/stakeholders/detail/stakeholderOrganizer.ts @@ -0,0 +1,84 @@ +import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; +import { Api_InterestHolder, Api_InterestHolderProperty } from '@/models/api/InterestHolder'; +import Api_TypeCode from '@/models/api/TypeCode'; + +import { InterestHolderViewForm, InterestHolderViewRow } from '../update/models'; + +class StakeholderOrganizer { + private readonly acquisitionFile: Api_AcquisitionFile; + private readonly interestHolders: Api_InterestHolder[] | undefined; + + constructor( + acquisitionFile: Api_AcquisitionFile, + interestHolders: Api_InterestHolder[] | undefined, + ) { + this.acquisitionFile = acquisitionFile; + this.interestHolders = interestHolders; + } + + getInterestProperties() { + const allInterestProperties = + this.interestHolders?.flatMap(interestHolder => interestHolder.interestHolderProperties) ?? + []; + + const interestProperties = allInterestProperties + .filter(ip => ip.propertyInterestTypes.some(pit => pit?.id !== 'NIP')) + .map(ip => { + const filteredTypes = ip.propertyInterestTypes.filter(pit => pit?.id !== 'NIP'); + return { ...ip, propertyInterestTypes: filteredTypes }; + }); + + return this.generateFormFromProperties(interestProperties); + } + + getNonInterestProperties() { + const allInterestProperties = + this.interestHolders?.flatMap(interestHolder => interestHolder.interestHolderProperties) ?? + []; + + const nonInterestProperties = allInterestProperties + .filter(ip => ip.propertyInterestTypes.some(pit => pit?.id === 'NIP')) + .map(ip => { + const filteredTypes = ip.propertyInterestTypes.filter(pit => pit?.id === 'NIP'); + return { ...ip, propertyInterestTypes: filteredTypes }; + }); + + return this.generateFormFromProperties(nonInterestProperties); + } + + private generateFormFromProperties(interestProperties: Api_InterestHolderProperty[]) { + const groupedInterestProperties: InterestHolderViewForm[] = []; + interestProperties.forEach((interestHolderProperty: Api_InterestHolderProperty) => { + const matchingGroup = groupedInterestProperties.find( + gip => gip.id === interestHolderProperty.acquisitionFilePropertyId, + ); + const matchingFileProperty = this.acquisitionFile.fileProperties?.find( + fp => fp.id === interestHolderProperty.acquisitionFilePropertyId, + ); + if (matchingFileProperty && interestHolderProperty) { + interestHolderProperty.acquisitionFileProperty = matchingFileProperty; + } + const interestHolder = this.interestHolders?.find( + ih => ih.interestHolderId === interestHolderProperty.interestHolderId, + ); + + if (!matchingGroup) { + const newGroup = InterestHolderViewForm.fromApi(interestHolderProperty); + newGroup.groupedPropertyInterests = interestHolderProperty.propertyInterestTypes.map( + (itc: Api_TypeCode) => + InterestHolderViewRow.fromApi(interestHolderProperty, interestHolder, itc), + ); + groupedInterestProperties.push(newGroup); + } else { + interestHolderProperty.propertyInterestTypes.forEach((itc: Api_TypeCode) => + matchingGroup.groupedPropertyInterests.push( + InterestHolderViewRow.fromApi(interestHolderProperty, interestHolder, itc), + ), + ); + } + }); + return groupedInterestProperties; + } +} + +export default StakeholderOrganizer; From 6a7a1a30ba07ed34fcc7f3af4f7c2b9a6e766edb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Nov 2023 00:39:03 +0000 Subject: [PATCH 02/24] CI: Bump version to v4.0.0-67.24 --- source/backend/api/Pims.Api.csproj | 4 ++-- source/frontend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 46e3733f1a..3b2da53822 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,8 +2,8 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 4.0.0-67.23 - 4.0.0-67.23 + 4.0.0-67.24 + 4.0.0-67.24 4.0.0.67 true 16BC0468-78F6-4C91-87DA-7403C919E646 diff --git a/source/frontend/package.json b/source/frontend/package.json index e0033f5e38..445276fc30 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "4.0.0-67.23", + "version": "4.0.0-67.24", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From c04cf147f7ea92d7fdadc6ae5faa80e75dc89808 Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Wed, 29 Nov 2023 10:51:36 -0800 Subject: [PATCH 03/24] Updated placeholder display (#3620) --- .../components/common/form/FastDatePicker.tsx | 2 +- .../AddFinancialCodeForm.test.tsx.snap | 4 ++-- .../UpdateFinancialCodeForm.test.tsx.snap | 4 ++-- .../AddLeaseContainer.test.tsx.snap | 6 +++--- .../AdministrationSubForm.test.tsx.snap | 2 +- .../LeaseDetailSubForm.test.tsx.snap | 4 ++-- .../ReceivedDepositForm.test.tsx.snap | 4 ++-- .../ReturnDepositForm.test.tsx.snap | 8 ++++---- .../__snapshots__/Insurance.test.tsx.snap | 2 +- .../EditInsuranceContainer.test.tsx.snap | 2 +- .../__snapshots__/PaymentForm.test.tsx.snap | 4 ++-- .../term/__snapshots__/TermForm.test.tsx.snap | 8 ++++---- .../AddAcquisitionContainer.test.tsx.snap | 4 ++-- .../AddAcquisitionForm.test.tsx.snap | 4 ++-- .../UpdateAgreementsForm.test.tsx.snap | 18 +++++++++--------- ...teCompensationRequisitionForm.test.tsx.snap | 8 ++++---- .../UpdateAcquisitionForm.test.tsx.snap | 6 +++--- .../__snapshots__/AddProjectForm.test.tsx.snap | 8 ++++---- .../TakesUpdateForm.test.tsx.snap | 6 +++--- .../UpdateSummaryForm.test.tsx.snap | 4 ++-- 20 files changed, 54 insertions(+), 54 deletions(-) diff --git a/source/frontend/src/components/common/form/FastDatePicker.tsx b/source/frontend/src/components/common/form/FastDatePicker.tsx index e6c7b2c2f6..45cab93e21 100644 --- a/source/frontend/src/components/common/form/FastDatePicker.tsx +++ b/source/frontend/src/components/common/form/FastDatePicker.tsx @@ -96,7 +96,7 @@ const FormikDatePicker: FunctionComponent @@ -462,7 +462,7 @@ exports[`AddFinancialCode form renders as expected 1`] = ` class="c4 form-control date-picker" id="datepicker-expiryDate" name="expiryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> diff --git a/source/frontend/src/features/admin/financial-codes/update/__snapshots__/UpdateFinancialCodeForm.test.tsx.snap b/source/frontend/src/features/admin/financial-codes/update/__snapshots__/UpdateFinancialCodeForm.test.tsx.snap index d53c0018e5..5e80927181 100644 --- a/source/frontend/src/features/admin/financial-codes/update/__snapshots__/UpdateFinancialCodeForm.test.tsx.snap +++ b/source/frontend/src/features/admin/financial-codes/update/__snapshots__/UpdateFinancialCodeForm.test.tsx.snap @@ -330,7 +330,7 @@ exports[`UpdateFinancialCode form renders as expected 1`] = ` class="c4 form-control date-picker" id="datepicker-effectiveDate" name="effectiveDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Nov 22, 1999" /> @@ -401,7 +401,7 @@ exports[`UpdateFinancialCode form renders as expected 1`] = ` class="c4 form-control date-picker" id="datepicker-expiryDate" name="expiryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> diff --git a/source/frontend/src/features/leases/add/__snapshots__/AddLeaseContainer.test.tsx.snap b/source/frontend/src/features/leases/add/__snapshots__/AddLeaseContainer.test.tsx.snap index 6f8e837afc..475f1aa572 100644 --- a/source/frontend/src/features/leases/add/__snapshots__/AddLeaseContainer.test.tsx.snap +++ b/source/frontend/src/features/leases/add/__snapshots__/AddLeaseContainer.test.tsx.snap @@ -704,7 +704,7 @@ exports[`AddLeaseContainer component renders as expected 1`] = ` class="c8 form-control date-picker" id="datepicker-startDate" name="startDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" @@ -756,7 +756,7 @@ exports[`AddLeaseContainer component renders as expected 1`] = ` class="c8 form-control date-picker" id="datepicker-expiryDate" name="expiryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -2225,7 +2225,7 @@ exports[`AddLeaseContainer component renders as expected 1`] = ` class="c8 form-control date-picker" id="datepicker-responsibilityEffectiveDate" name="responsibilityEffectiveDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> diff --git a/source/frontend/src/features/leases/add/__snapshots__/AdministrationSubForm.test.tsx.snap b/source/frontend/src/features/leases/add/__snapshots__/AdministrationSubForm.test.tsx.snap index 54c494000a..69f66d136e 100644 --- a/source/frontend/src/features/leases/add/__snapshots__/AdministrationSubForm.test.tsx.snap +++ b/source/frontend/src/features/leases/add/__snapshots__/AdministrationSubForm.test.tsx.snap @@ -941,7 +941,7 @@ exports[`AdministrationSubForm component renders as expected 1`] = ` class="c6 form-control date-picker" id="datepicker-responsibilityEffectiveDate" name="responsibilityEffectiveDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> diff --git a/source/frontend/src/features/leases/add/__snapshots__/LeaseDetailSubForm.test.tsx.snap b/source/frontend/src/features/leases/add/__snapshots__/LeaseDetailSubForm.test.tsx.snap index 2e8ae5c784..bfb0ed1d76 100644 --- a/source/frontend/src/features/leases/add/__snapshots__/LeaseDetailSubForm.test.tsx.snap +++ b/source/frontend/src/features/leases/add/__snapshots__/LeaseDetailSubForm.test.tsx.snap @@ -298,7 +298,7 @@ exports[`LeaseDetailSubForm component renders as expected 1`] = ` class="c5 form-control date-picker" id="datepicker-startDate" name="startDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" @@ -350,7 +350,7 @@ exports[`LeaseDetailSubForm component renders as expected 1`] = ` class="c5 form-control date-picker" id="datepicker-expiryDate" name="expiryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> diff --git a/source/frontend/src/features/leases/detail/LeasePages/deposits/modal/receivedDepositModal/__snapshots__/ReceivedDepositForm.test.tsx.snap b/source/frontend/src/features/leases/detail/LeasePages/deposits/modal/receivedDepositModal/__snapshots__/ReceivedDepositForm.test.tsx.snap index 0f4710246b..4c21d40fcf 100644 --- a/source/frontend/src/features/leases/detail/LeasePages/deposits/modal/receivedDepositModal/__snapshots__/ReceivedDepositForm.test.tsx.snap +++ b/source/frontend/src/features/leases/detail/LeasePages/deposits/modal/receivedDepositModal/__snapshots__/ReceivedDepositForm.test.tsx.snap @@ -390,7 +390,7 @@ exports[`ReceivedDepositForm component renders as expected 1`] = ` class="c4 form-control date-picker" id="datepicker-depositDate" name="depositDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" @@ -893,7 +893,7 @@ exports[`ReceivedDepositForm component renders with data as expected 1`] = ` class="c4 form-control date-picker" id="datepicker-depositDate" name="depositDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" diff --git a/source/frontend/src/features/leases/detail/LeasePages/deposits/modal/returnedDepositModal/__snapshots__/ReturnDepositForm.test.tsx.snap b/source/frontend/src/features/leases/detail/LeasePages/deposits/modal/returnedDepositModal/__snapshots__/ReturnDepositForm.test.tsx.snap index ffe7cbc887..8d1b2fb628 100644 --- a/source/frontend/src/features/leases/detail/LeasePages/deposits/modal/returnedDepositModal/__snapshots__/ReturnDepositForm.test.tsx.snap +++ b/source/frontend/src/features/leases/detail/LeasePages/deposits/modal/returnedDepositModal/__snapshots__/ReturnDepositForm.test.tsx.snap @@ -311,7 +311,7 @@ exports[`ReturnDepositForm component renders as expected 1`] = ` class="c3 form-control date-picker" id="datepicker-terminationDate" name="terminationDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" @@ -496,7 +496,7 @@ exports[`ReturnDepositForm component renders as expected 1`] = ` class="c3 form-control date-picker" id="datepicker-returnDate" name="returnDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" @@ -920,7 +920,7 @@ exports[`ReturnDepositForm component renders with data as expected 1`] = ` class="c3 form-control date-picker" id="datepicker-terminationDate" name="terminationDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" @@ -1105,7 +1105,7 @@ exports[`ReturnDepositForm component renders with data as expected 1`] = ` class="c3 form-control date-picker" id="datepicker-returnDate" name="returnDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" diff --git a/source/frontend/src/features/leases/detail/LeasePages/insurance/details/__snapshots__/Insurance.test.tsx.snap b/source/frontend/src/features/leases/detail/LeasePages/insurance/details/__snapshots__/Insurance.test.tsx.snap index f912b33cb9..28e3ad493e 100644 --- a/source/frontend/src/features/leases/detail/LeasePages/insurance/details/__snapshots__/Insurance.test.tsx.snap +++ b/source/frontend/src/features/leases/detail/LeasePages/insurance/details/__snapshots__/Insurance.test.tsx.snap @@ -291,7 +291,7 @@ exports[`Edit Lease Insurance renders as expected 1`] = ` class="c4 form-control date-picker" id="datepicker-insurances.0.expiryDate" name="insurances.0.expiryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Jan 01, 2022" /> diff --git a/source/frontend/src/features/leases/detail/LeasePages/insurance/edit/__snapshots__/EditInsuranceContainer.test.tsx.snap b/source/frontend/src/features/leases/detail/LeasePages/insurance/edit/__snapshots__/EditInsuranceContainer.test.tsx.snap index efe43fde9e..35db7470b0 100644 --- a/source/frontend/src/features/leases/detail/LeasePages/insurance/edit/__snapshots__/EditInsuranceContainer.test.tsx.snap +++ b/source/frontend/src/features/leases/detail/LeasePages/insurance/edit/__snapshots__/EditInsuranceContainer.test.tsx.snap @@ -291,7 +291,7 @@ exports[`Edit Lease Insurance renders as expected 1`] = ` class="c4 form-control date-picker" id="datepicker-insurances.0.expiryDate" name="insurances.0.expiryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Jan 01, 2022" /> diff --git a/source/frontend/src/features/leases/detail/LeasePages/payment/modal/payment/__snapshots__/PaymentForm.test.tsx.snap b/source/frontend/src/features/leases/detail/LeasePages/payment/modal/payment/__snapshots__/PaymentForm.test.tsx.snap index 107e8f42ca..d4e14d69e3 100644 --- a/source/frontend/src/features/leases/detail/LeasePages/payment/modal/payment/__snapshots__/PaymentForm.test.tsx.snap +++ b/source/frontend/src/features/leases/detail/LeasePages/payment/modal/payment/__snapshots__/PaymentForm.test.tsx.snap @@ -151,7 +151,7 @@ exports[`PaymentForm component renders as expected 1`] = ` class="c2 form-control date-picker" id="datepicker-receivedDate" name="receivedDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" @@ -480,7 +480,7 @@ exports[`PaymentForm component renders with data as expected 1`] = ` class="c2 form-control date-picker" id="datepicker-receivedDate" name="receivedDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" diff --git a/source/frontend/src/features/leases/detail/LeasePages/payment/modal/term/__snapshots__/TermForm.test.tsx.snap b/source/frontend/src/features/leases/detail/LeasePages/payment/modal/term/__snapshots__/TermForm.test.tsx.snap index 3d525ad352..58398a81f6 100644 --- a/source/frontend/src/features/leases/detail/LeasePages/payment/modal/term/__snapshots__/TermForm.test.tsx.snap +++ b/source/frontend/src/features/leases/detail/LeasePages/payment/modal/term/__snapshots__/TermForm.test.tsx.snap @@ -127,7 +127,7 @@ exports[`TermForm component renders as expected 1`] = ` class="c3 form-control date-picker" id="datepicker-startDate" name="startDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" @@ -164,7 +164,7 @@ exports[`TermForm component renders as expected 1`] = ` class="c3 form-control date-picker" id="datepicker-expiryDate" name="expiryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -535,7 +535,7 @@ exports[`TermForm component renders with data as expected 1`] = ` class="c3 form-control date-picker" id="datepicker-startDate" name="startDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" required="" type="text" value="" @@ -572,7 +572,7 @@ exports[`TermForm component renders with data as expected 1`] = ` class="c3 form-control date-picker" id="datepicker-expiryDate" name="expiryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AddAcquisitionContainer.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AddAcquisitionContainer.test.tsx.snap index 1c58ee6400..532b4de7f7 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AddAcquisitionContainer.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AddAcquisitionContainer.test.tsx.snap @@ -748,7 +748,7 @@ exports[`AddAcquisitionContainer component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-assignedDate" name="assignedDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -795,7 +795,7 @@ exports[`AddAcquisitionContainer component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-deliveryDate" name="deliveryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AddAcquisitionForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AddAcquisitionForm.test.tsx.snap index ce9150d8a5..49f878fb21 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AddAcquisitionForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AddAcquisitionForm.test.tsx.snap @@ -637,7 +637,7 @@ exports[`AddAcquisitionForm component renders as expected 1`] = ` class="c7 form-control date-picker" id="datepicker-assignedDate" name="assignedDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -684,7 +684,7 @@ exports[`AddAcquisitionForm component renders as expected 1`] = ` class="c7 form-control date-picker" id="datepicker-deliveryDate" name="deliveryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/__snapshots__/UpdateAgreementsForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/__snapshots__/UpdateAgreementsForm.test.tsx.snap index 02de037081..db3d48b5a8 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/__snapshots__/UpdateAgreementsForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/update/__snapshots__/UpdateAgreementsForm.test.tsx.snap @@ -548,7 +548,7 @@ exports[`UpdateAgreementsForm component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-agreements.0.agreementDate" name="agreements.0.agreementDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -595,7 +595,7 @@ exports[`UpdateAgreementsForm component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-agreements.0.commencementDate" name="agreements.0.commencementDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -642,7 +642,7 @@ exports[`UpdateAgreementsForm component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-agreements.0.completionDate" name="agreements.0.completionDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -689,7 +689,7 @@ exports[`UpdateAgreementsForm component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-agreements.0.terminationDate" name="agreements.0.terminationDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -736,7 +736,7 @@ exports[`UpdateAgreementsForm component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-agreements.0.possessionDate" name="agreements.0.possessionDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -1138,7 +1138,7 @@ exports[`UpdateAgreementsForm component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-agreements.1.agreementDate" name="agreements.1.agreementDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Apr 05, 2023" /> @@ -1185,7 +1185,7 @@ exports[`UpdateAgreementsForm component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-agreements.1.completionDate" name="agreements.1.completionDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Apr 05, 2023" /> @@ -1232,7 +1232,7 @@ exports[`UpdateAgreementsForm component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-agreements.1.terminationDate" name="agreements.1.terminationDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Apr 05, 2023" /> @@ -1279,7 +1279,7 @@ exports[`UpdateAgreementsForm component renders as expected 1`] = ` class="c10 form-control date-picker" id="datepicker-agreements.1.possessionDate" name="agreements.1.possessionDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Apr 05, 2023" /> diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/update/__snapshots__/UpdateCompensationRequisitionForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/update/__snapshots__/UpdateCompensationRequisitionForm.test.tsx.snap index fa9ac6c9cd..5e3072bedb 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/update/__snapshots__/UpdateCompensationRequisitionForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/update/__snapshots__/UpdateCompensationRequisitionForm.test.tsx.snap @@ -438,7 +438,7 @@ exports[`Compensation Requisition UpdateForm component renders as expected 1`] = class="c7 form-control date-picker" id="datepicker-agreementDateTime" name="agreementDateTime" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -485,7 +485,7 @@ exports[`Compensation Requisition UpdateForm component renders as expected 1`] = class="c7 form-control date-picker" id="datepicker-expropriationNoticeServedDateTime" name="expropriationNoticeServedDateTime" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -532,7 +532,7 @@ exports[`Compensation Requisition UpdateForm component renders as expected 1`] = class="c7 form-control date-picker" id="datepicker-expropriationVestingDateTime" name="expropriationVestingDateTime" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> @@ -579,7 +579,7 @@ exports[`Compensation Requisition UpdateForm component renders as expected 1`] = class="c7 form-control date-picker" id="datepicker-advancedPaymentServedDate" name="advancedPaymentServedDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/__snapshots__/UpdateAcquisitionForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/__snapshots__/UpdateAcquisitionForm.test.tsx.snap index 2cd9aec140..c267a26dfb 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/__snapshots__/UpdateAcquisitionForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/__snapshots__/UpdateAcquisitionForm.test.tsx.snap @@ -774,7 +774,7 @@ exports[`UpdateAcquisitionForm component renders as expected 1`] = ` class="c7 form-control date-picker" id="datepicker-assignedDate" name="assignedDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Jun 27, 2022" /> @@ -845,7 +845,7 @@ exports[`UpdateAcquisitionForm component renders as expected 1`] = ` class="c7 form-control date-picker" id="datepicker-deliveryDate" name="deliveryDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Jul 29, 2022" /> @@ -917,7 +917,7 @@ exports[`UpdateAcquisitionForm component renders as expected 1`] = ` disabled="" id="datepicker-completionDate" name="completionDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="" /> diff --git a/source/frontend/src/features/mapSideBar/project/add/__snapshots__/AddProjectForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/project/add/__snapshots__/AddProjectForm.test.tsx.snap index 6dd083938f..dde9d36ffb 100644 --- a/source/frontend/src/features/mapSideBar/project/add/__snapshots__/AddProjectForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/project/add/__snapshots__/AddProjectForm.test.tsx.snap @@ -1454,7 +1454,7 @@ exports[`AddProjectForm component renders as expected with existing data 1`] = ` class="c10 form-control date-picker" id="datepicker-products.0.startDate" name="products.0.startDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Feb 01, 2023" /> @@ -1545,7 +1545,7 @@ exports[`AddProjectForm component renders as expected with existing data 1`] = ` class="c10 form-control date-picker" id="datepicker-products.0.costEstimateDate" name="products.0.costEstimateDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Feb 02, 2023" /> @@ -1761,7 +1761,7 @@ exports[`AddProjectForm component renders as expected with existing data 1`] = ` class="c10 form-control date-picker" id="datepicker-products.1.startDate" name="products.1.startDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Feb 03, 2023" /> @@ -1852,7 +1852,7 @@ exports[`AddProjectForm component renders as expected with existing data 1`] = ` class="c10 form-control date-picker" id="datepicker-products.1.costEstimateDate" name="products.1.costEstimateDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Feb 04, 2023" /> diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/__snapshots__/TakesUpdateForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/property/tabs/takes/update/__snapshots__/TakesUpdateForm.test.tsx.snap index fa4cd62da8..2ec4298ea7 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/__snapshots__/TakesUpdateForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/update/__snapshots__/TakesUpdateForm.test.tsx.snap @@ -1170,7 +1170,7 @@ exports[`TakesUpdateForm component renders as expected 1`] = ` class="c18 form-control date-picker" id="datepicker-takes.0.srwEndDt" name="takes.0.srwEndDt" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Nov 20, 2022" /> @@ -1483,7 +1483,7 @@ exports[`TakesUpdateForm component renders as expected 1`] = ` class="c18 form-control date-picker" id="datepicker-takes.0.landActEndDt" name="takes.0.landActEndDt" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Jan 01, 2020" /> @@ -1729,7 +1729,7 @@ exports[`TakesUpdateForm component renders as expected 1`] = ` class="c18 form-control date-picker" id="datepicker-takes.0.ltcEndDt" name="takes.0.ltcEndDt" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Jan 01, 2020" /> diff --git a/source/frontend/src/features/mapSideBar/research/tabs/fileDetails/update/__snapshots__/UpdateSummaryForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/research/tabs/fileDetails/update/__snapshots__/UpdateSummaryForm.test.tsx.snap index 9b4542cf6d..8c61a02407 100644 --- a/source/frontend/src/features/mapSideBar/research/tabs/fileDetails/update/__snapshots__/UpdateSummaryForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/research/tabs/fileDetails/update/__snapshots__/UpdateSummaryForm.test.tsx.snap @@ -774,7 +774,7 @@ exports[`UpdateResearchForm component renders as expected when provided no resea class="c13 form-control date-picker" id="datepicker-requestDate" name="requestDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Apr 14, 2022" /> @@ -1070,7 +1070,7 @@ exports[`UpdateResearchForm component renders as expected when provided no resea class="c13 form-control date-picker" id="datepicker-researchCompletionDate" name="researchCompletionDate" - placeholder="MTH DD, YYYY" + placeholder="MMM DD, YYYY" type="text" value="Mar 30, 2022" /> From 694443e9e49a4d9e1de1f3b66bd41395f7112386 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:52:01 +0000 Subject: [PATCH 04/24] CI: Bump version to v4.0.0-67.25 --- source/backend/api/Pims.Api.csproj | 4 ++-- source/frontend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 3b2da53822..b10d75ea6b 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,8 +2,8 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 4.0.0-67.24 - 4.0.0-67.24 + 4.0.0-67.25 + 4.0.0-67.25 4.0.0.67 true 16BC0468-78F6-4C91-87DA-7403C919E646 diff --git a/source/frontend/package.json b/source/frontend/package.json index 445276fc30..ebb33b8e86 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "4.0.0-67.24", + "version": "4.0.0-67.25", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From b2a45a0ebe37dc275b909aa9456f118b8fc2132d Mon Sep 17 00:00:00 2001 From: devinleighsmith <41091511+devinleighsmith@users.noreply.github.com> Date: Fri, 1 Dec 2023 12:06:33 -0800 Subject: [PATCH 05/24] dialog correction. (#3629) Co-authored-by: Smith --- .../update/UpdateCompensationRequisitionForm.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/update/UpdateCompensationRequisitionForm.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/update/UpdateCompensationRequisitionForm.tsx index 1ae3177386..88dfdf695e 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/update/UpdateCompensationRequisitionForm.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/update/UpdateCompensationRequisitionForm.tsx @@ -350,10 +350,9 @@ const UpdateCompensationRequisitionForm: React.FCError: , - `You have selected an alternate project that is the same as the file project, please select a different project`, + `You have selected an alternate project that is the same as the file project, please select a different project.`, ]} - okButtonText="Ok" + okButtonText="Close" handleOk={() => { setShowAltProjectError(false); formikRef.current?.setFieldValue('alternateProject', ''); From 9be0933fe918fda3b8a501e0a7f8f2c15095a160 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:06:54 +0000 Subject: [PATCH 06/24] CI: Bump version to v4.0.0-67.26 --- source/backend/api/Pims.Api.csproj | 4 ++-- source/frontend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index b10d75ea6b..037d2d1246 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,8 +2,8 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 4.0.0-67.25 - 4.0.0-67.25 + 4.0.0-67.26 + 4.0.0-67.26 4.0.0.67 true 16BC0468-78F6-4C91-87DA-7403C919E646 diff --git a/source/frontend/package.json b/source/frontend/package.json index ebb33b8e86..e9d7ccc304 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "4.0.0-67.25", + "version": "4.0.0-67.26", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From 26c199e4a2eabce86b602d4b6a7681c34e5e05ea Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Fri, 1 Dec 2023 13:24:59 -0800 Subject: [PATCH 07/24] Keycloak Refactor (#3624) * Refactored keycloak sync to use the keycloak repository used by the API. ALso removed tools.Core and removed redundant models * pr fixes --- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../dal.keycloak/PimsKeycloakService.cs | 16 +- .../Extensions/ServiceCollectionExtensions.cs | 4 +- .../backend/keycloak/IKeycloakRepository.cs | 40 ++++ source/backend/keycloak/IKeycloakService.cs | 19 -- source/backend/keycloak/KeycloakRepository.cs | 179 +++++++++++++++ source/backend/keycloak/KeycloakService.cs | 47 ---- source/backend/keycloak/Models/RoleModel.cs | 2 +- .../keycloak/Partials/KeycloakServiceUsers.cs | 76 ------- .../Libraries/Keycloak/KeycloakServiceTest.cs | 16 +- .../Keycloak/PimsKeycloakUserServiceTest.cs | 28 +-- .../ServiceCollectionExtensionsTest.cs | 6 +- .../Configuration/KeycloakAdminOptions.cs | 28 --- .../Keycloak/Configuration/KeycloakOptions.cs | 18 -- tools/core/Keycloak/IKeycloakRequestClient.cs | 14 -- tools/core/Keycloak/KeycloakRequestClient.cs | 82 ------- tools/core/Keycloak/Models/ClientModel.cs | 206 ------------------ tools/core/Keycloak/Models/GroupModel.cs | 59 ----- .../Keycloak/Models/ProtocolMapperModel.cs | 48 ---- .../Keycloak/Models/ResponseWrapperModel.cs | 9 - tools/core/Keycloak/Models/RoleModel.cs | 31 --- tools/core/Keycloak/Models/UserModel.cs | 59 ----- tools/core/Keycloak/Models/UserRoleModel.cs | 16 -- .../core/Keycloak/Models/UserRoleOperation.cs | 26 --- tools/core/Pims.Tools.Core.csproj | 29 --- .../sync/Client}/IRequestClient.cs | 2 +- .../keycloak/sync/Client/PimsRequestClient.cs | 4 +- .../sync/Client}/RequestClient.cs | 5 +- .../sync/Configuration/AuthOptions.cs | 2 +- .../sync}/Configuration/RequestOptions.cs | 2 +- .../sync/Configuration/ToolOptions.cs | 1 - .../sync/Models/Keycloak/ClientModel.cs | 186 +++++++++++++++- .../sync/Models/Keycloak/GroupModel.cs | 39 +++- .../Models/Keycloak/ProtocolMapperModel.cs | 29 ++- .../sync/Models/Keycloak}/RealmModel.cs | 0 .../sync/Models/Keycloak/RoleModel.cs | 32 --- .../sync/Pims.Tools.Keycloak.Sync.csproj | 2 +- .../sync/Pims.Tools.Keycloak.Sync.sln | 8 +- tools/keycloak/sync/Program.cs | 11 +- .../sync/{ => Syncronizer}/ISyncFactory.cs | 0 .../sync/{ => Syncronizer}/SyncFactory.cs | 76 +++---- 41 files changed, 560 insertions(+), 899 deletions(-) create mode 100644 source/backend/keycloak/IKeycloakRepository.cs delete mode 100644 source/backend/keycloak/IKeycloakService.cs create mode 100644 source/backend/keycloak/KeycloakRepository.cs delete mode 100644 source/backend/keycloak/KeycloakService.cs delete mode 100644 source/backend/keycloak/Partials/KeycloakServiceUsers.cs delete mode 100644 tools/core/Keycloak/Configuration/KeycloakAdminOptions.cs delete mode 100644 tools/core/Keycloak/Configuration/KeycloakOptions.cs delete mode 100644 tools/core/Keycloak/IKeycloakRequestClient.cs delete mode 100644 tools/core/Keycloak/KeycloakRequestClient.cs delete mode 100644 tools/core/Keycloak/Models/ClientModel.cs delete mode 100644 tools/core/Keycloak/Models/GroupModel.cs delete mode 100644 tools/core/Keycloak/Models/ProtocolMapperModel.cs delete mode 100644 tools/core/Keycloak/Models/ResponseWrapperModel.cs delete mode 100644 tools/core/Keycloak/Models/RoleModel.cs delete mode 100644 tools/core/Keycloak/Models/UserModel.cs delete mode 100644 tools/core/Keycloak/Models/UserRoleModel.cs delete mode 100644 tools/core/Keycloak/Models/UserRoleOperation.cs delete mode 100644 tools/core/Pims.Tools.Core.csproj rename tools/{core => keycloak/sync/Client}/IRequestClient.cs (97%) rename tools/{core => keycloak/sync/Client}/RequestClient.cs (98%) rename tools/{core => keycloak/sync}/Configuration/RequestOptions.cs (94%) rename tools/{core/Keycloak/Models => keycloak/sync/Models/Keycloak}/RealmModel.cs (100%) delete mode 100644 tools/keycloak/sync/Models/Keycloak/RoleModel.cs rename tools/keycloak/sync/{ => Syncronizer}/ISyncFactory.cs (100%) rename tools/keycloak/sync/{ => Syncronizer}/SyncFactory.cs (77%) diff --git a/source/backend/dal.keycloak/Extensions/ServiceCollectionExtensions.cs b/source/backend/dal.keycloak/Extensions/ServiceCollectionExtensions.cs index 0e2342285f..76ce80847c 100644 --- a/source/backend/dal.keycloak/Extensions/ServiceCollectionExtensions.cs +++ b/source/backend/dal.keycloak/Extensions/ServiceCollectionExtensions.cs @@ -16,7 +16,7 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddPimsKeycloakService(this IServiceCollection services) { return services.AddScoped() - .AddScoped(); + .AddScoped(); } } } diff --git a/source/backend/dal.keycloak/PimsKeycloakService.cs b/source/backend/dal.keycloak/PimsKeycloakService.cs index fa6669c39d..fc5381b454 100644 --- a/source/backend/dal.keycloak/PimsKeycloakService.cs +++ b/source/backend/dal.keycloak/PimsKeycloakService.cs @@ -20,7 +20,7 @@ namespace Pims.Dal.Keycloak public class PimsKeycloakService : IPimsKeycloakService { #region Variable - private readonly IKeycloakService _keycloakService; + private readonly IKeycloakRepository _keycloakRepository; private readonly IUserRepository _userRepository; private readonly IRoleRepository _roleRepository; private readonly IAccessRequestRepository _accessRequestRepository; @@ -33,19 +33,19 @@ public class PimsKeycloakService : IPimsKeycloakService /// /// Creates a new instance of a PimsKeycloakService object, initializes with the specified arguments. /// - /// + /// /// /// /// /// public PimsKeycloakService( - IKeycloakService keycloakService, + IKeycloakRepository keycloakRepository, IUserRepository userRepository, IRoleRepository roleRepository, IAccessRequestRepository accessRequestRepository, ClaimsPrincipal user) { - _keycloakService = keycloakService; + _keycloakRepository = keycloakRepository; _userRepository = userRepository; _roleRepository = roleRepository; _accessRequestRepository = accessRequestRepository; @@ -63,7 +63,7 @@ public PimsKeycloakService( /// public async Task UpdateUserAsync(Entity.PimsUser user) { - var kuser = await _keycloakService.GetUserAsync(user.GuidIdentifierValue.Value) ?? throw new KeyNotFoundException("User does not exist in Keycloak"); + var kuser = await _keycloakRepository.GetUserAsync(user.GuidIdentifierValue.Value) ?? throw new KeyNotFoundException("User does not exist in Keycloak"); var euser = _userRepository.GetTrackingById(user.Internal_Id); return await SaveUserChanges(user, euser, kuser, true); @@ -77,7 +77,7 @@ public PimsKeycloakService( /// public async Task AppendToUserAsync(Entity.PimsUser update) { - var kuser = await _keycloakService.GetUserAsync(update.GuidIdentifierValue.Value) ?? throw new KeyNotFoundException("User does not exist in Keycloak"); + var kuser = await _keycloakRepository.GetUserAsync(update.GuidIdentifierValue.Value) ?? throw new KeyNotFoundException("User does not exist in Keycloak"); var euser = _userRepository.GetTrackingById(update.Internal_Id); return await SaveUserChanges(update, euser, kuser, true); @@ -165,13 +165,13 @@ public PimsKeycloakService( var roles = update.IsDisabled.HasValue && update.IsDisabled.Value ? System.Array.Empty() : euser.PimsUserRoles.Select(ur => _roleRepository.Find(ur.RoleId)); // Now update keycloak - var keycloakUserGroups = await _keycloakService.GetUserGroupsAsync(euser.GuidIdentifierValue.Value); + var keycloakUserGroups = await _keycloakRepository.GetUserGroupsAsync(euser.GuidIdentifierValue.Value); var newRolesToAdd = roles.Where(r => keycloakUserGroups.All(crr => crr.Name != r.Name)); var rolesToRemove = keycloakUserGroups.Where(r => roles.All(crr => crr.Name != r.Name)); var addOperations = newRolesToAdd.Select(nr => new UserRoleOperation() { Operation = "add", RoleName = nr.Name, Username = update.GetIdirUsername() }); var removeOperations = rolesToRemove.Select(rr => new UserRoleOperation() { Operation = "del", RoleName = rr.Name, Username = update.GetIdirUsername() }); - await _keycloakService.ModifyUserRoleMappings(addOperations.Concat(removeOperations)); + await _keycloakRepository.ModifyUserRoleMappings(addOperations.Concat(removeOperations)); _userRepository.CommitTransaction(); return _userRepository.GetById(euser.Internal_Id); diff --git a/source/backend/keycloak/Extensions/ServiceCollectionExtensions.cs b/source/backend/keycloak/Extensions/ServiceCollectionExtensions.cs index aec90e7bfb..00bea060b9 100644 --- a/source/backend/keycloak/Extensions/ServiceCollectionExtensions.cs +++ b/source/backend/keycloak/Extensions/ServiceCollectionExtensions.cs @@ -12,9 +12,9 @@ public static class ServiceCollectionExtensions ///
/// /// - public static IServiceCollection AddKeycloakService(this IServiceCollection services) + public static IServiceCollection AddKeycloakRepository(this IServiceCollection services) { - return services.AddScoped(); + return services.AddScoped(); } } } diff --git a/source/backend/keycloak/IKeycloakRepository.cs b/source/backend/keycloak/IKeycloakRepository.cs new file mode 100644 index 0000000000..bec4b6fba6 --- /dev/null +++ b/source/backend/keycloak/IKeycloakRepository.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Pims.Keycloak.Models; + +namespace Pims.Keycloak +{ + public interface IKeycloakRepository + { + #region Users + + Task GetUserAsync(Guid id); + + Task> GetUsersAsync(Guid id); + + Task AddRolesToUser(string username, IEnumerable roles); + + Task DeleteRoleFromUsers(string username, string roleName); + + Task GetUserGroupsAsync(Guid id); + + Task ModifyUserRoleMappings(IEnumerable operations); + + Task> GetAllRoles(); + + Task> GetAllGroupRoles(string groupName); + + Task> GetUserRoles(string username); + + Task AddKeycloakRole(RoleModel role); + + Task AddKeycloakRolesToGroup(string groupName, IEnumerable roles); + + Task DeleteRole(string roleName); + + Task DeleteRoleFromGroup(string groupName, string roleName); + #endregion + } +} diff --git a/source/backend/keycloak/IKeycloakService.cs b/source/backend/keycloak/IKeycloakService.cs deleted file mode 100644 index 467897c8a8..0000000000 --- a/source/backend/keycloak/IKeycloakService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Pims.Keycloak.Models; - -namespace Pims.Keycloak -{ - public interface IKeycloakService - { - #region Users - - Task GetUserAsync(Guid id); - - Task GetUserGroupsAsync(Guid id); - - Task ModifyUserRoleMappings(IEnumerable operations); - #endregion - } -} diff --git a/source/backend/keycloak/KeycloakRepository.cs b/source/backend/keycloak/KeycloakRepository.cs new file mode 100644 index 0000000000..10c1177650 --- /dev/null +++ b/source/backend/keycloak/KeycloakRepository.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Pims.Core.Extensions; +using Pims.Core.Http; +using Pims.Keycloak.Extensions; +using Pims.Keycloak.Models; + +namespace Pims.Keycloak +{ + /// + /// KeycloakRepository class, provides a service for sending HTTP requests to the keycloak admin API. + /// - https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_overview. + /// + public partial class KeycloakRepository : IKeycloakRepository + { + #region Variables + private readonly IOpenIdConnectRequestClient _client; + #endregion + + #region Properties + + /// + /// get - The configuration options for keycloak. + /// + public Configuration.KeycloakOptions Options { get; } + #endregion + + #region Constructors + + /// + /// Creates a new instance of a KeycloakAdmin class, initializes it with the specified arguments. + /// + /// + /// + public KeycloakRepository(IOpenIdConnectRequestClient client, IOptions options) + { + this.Options = options.Value; + this.Options.Validate(); + this.Options.ServiceAccount.Validate(); + _client = client; + _client.AuthClientOptions.Audience = this.Options.ServiceAccount.Audience ?? this.Options.Audience; + _client.AuthClientOptions.Authority = this.Options.ServiceAccount.Authority ?? this.Options.Authority; + _client.AuthClientOptions.Client = this.Options.ServiceAccount.Client; + _client.AuthClientOptions.Secret = this.Options.ServiceAccount.Secret; + } + + /// + /// Get the user for the specified 'id'. + /// + /// + /// + public async Task GetUserAsync(Guid id) + { + var users = await GetUsersAsync(id); + return users.FirstOrDefault(); + } + + public async Task> GetUsersAsync(Guid id) + { + var response = await _client.GetAsync($"{this.Options.ServiceAccount.Api}/{this.Options.ServiceAccount.Environment}/idir/users?guid={id.ToString().Replace("-", string.Empty)}"); + var result = await response.HandleResponseAsync>(); + + return result.Data.ToList(); + } + + public async Task AddRolesToUser(string username, IEnumerable roles) + { + return await _client.PostJsonAsync($"{GetIntegrationUrl()}/users/{Uri.EscapeDataString(username)}/roles", roles); + } + + public async Task DeleteRoleFromUsers(string username, string roleName) + { + return await _client.DeleteAsync($"{GetIntegrationUrl()}/users/{Uri.EscapeDataString(username)}/roles/{Uri.EscapeDataString(roleName)}"); + } + + /// + /// Get an array of the groups the user for the specified 'id' is a member of. + /// + /// + /// + public async Task GetUserGroupsAsync(Guid id) + { + var response = await _client.GetAsync($"{GetIntegrationUrl()}/user-role-mappings/?username={id.ToString().Replace("-", string.Empty)}@idir"); + + var userRoleModel = await response.HandleResponseAsync(); + + return userRoleModel.Roles.Where(r => r.Composite.HasValue && r.Composite.Value).ToArray(); + } + + /// + /// Get the total number of groups the user for the specified 'id' is a member of. + /// + /// + /// + public async Task GetUserGroupCountAsync(Guid id) + { + var response = await GetUserGroupsAsync(id); + return response.Length; + } + + /// + /// execute all passed operations. + /// + /// + /// + public async Task ModifyUserRoleMappings(IEnumerable operations) + { + foreach (UserRoleOperation operation in operations) + { + var json = operation.Serialize(); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await _client.PostAsync($"{GetIntegrationUrl()}/user-role-mappings", content); + await response.HandleResponseAsync(); + } + } + + public async Task> GetAllRoles() + { + var response = await _client.GetAsync($"{GetIntegrationUrl()}/roles"); + + var allKeycloakRoles = await response.HandleResponseAsync>(); + return allKeycloakRoles; + } + + public async Task> GetAllGroupRoles(string groupName) + { + var response = await _client.GetAsync($"{GetIntegrationUrl()}/roles/{Uri.EscapeDataString(groupName)}/composite-roles"); + + var groupedRoles = await response.HandleResponseAsync>(); + return groupedRoles; + } + + public async Task> GetUserRoles(string username) + { + var response = await _client.GetAsync($"{GetIntegrationUrl()}/users/{Uri.EscapeDataString(username)}/roles"); + + var groupedRoles = await response.HandleResponseAsync>(); + return groupedRoles; + } + + public async Task AddKeycloakRole(RoleModel role) + { + var response = await _client.PostJsonAsync($"{GetIntegrationUrl()}/roles", role); + return response; + } + + public async Task AddKeycloakRolesToGroup(string groupName, IEnumerable roles) + { + var response = await _client.PostJsonAsync($"{GetIntegrationUrl()}/roles/{Uri.EscapeDataString(groupName)}/composite-roles", roles); + return response; + } + + public async Task DeleteRole(string roleName) + { + var response = await _client.DeleteAsync($"{GetIntegrationUrl()}/roles/{Uri.EscapeDataString(roleName)}"); + return response; + } + + public async Task DeleteRoleFromGroup(string groupName, string roleName) + { + var response = await _client.DeleteAsync($"{GetIntegrationUrl()}/roles/{Uri.EscapeDataString(groupName)}/composite-roles/{Uri.EscapeDataString(roleName)}"); + return response; + } + + private string GetIntegrationUrl() + { + return $"{this.Options.ServiceAccount.Api}/integrations/{this.Options.ServiceAccount.Integration}/{this.Options.ServiceAccount.Environment}"; + } + #endregion + + #region Methods + #endregion + } +} diff --git a/source/backend/keycloak/KeycloakService.cs b/source/backend/keycloak/KeycloakService.cs deleted file mode 100644 index c36c23fcf5..0000000000 --- a/source/backend/keycloak/KeycloakService.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.Extensions.Options; -using Pims.Core.Http; - -namespace Pims.Keycloak -{ - /// - /// KeycloakService class, provides a service for sending HTTP requests to the keycloak admin API. - /// - https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_overview. - /// - public partial class KeycloakService : IKeycloakService - { - #region Variables - private readonly IOpenIdConnectRequestClient _client; - #endregion - - #region Properties - - /// - /// get - The configuration options for keycloak. - /// - public Configuration.KeycloakOptions Options { get; } - #endregion - - #region Constructors - - /// - /// Creates a new instance of a KeycloakAdmin class, initializes it with the specified arguments. - /// - /// - /// - public KeycloakService(IOpenIdConnectRequestClient client, IOptions options) - { - this.Options = options.Value; - this.Options.Validate(); - this.Options.ServiceAccount.Validate(); - _client = client; - _client.AuthClientOptions.Audience = this.Options.ServiceAccount.Audience ?? this.Options.Audience; - _client.AuthClientOptions.Authority = this.Options.ServiceAccount.Authority ?? this.Options.Authority; - _client.AuthClientOptions.Client = this.Options.ServiceAccount.Client; - _client.AuthClientOptions.Secret = this.Options.ServiceAccount.Secret; - } - #endregion - - #region Methods - #endregion - } -} diff --git a/source/backend/keycloak/Models/RoleModel.cs b/source/backend/keycloak/Models/RoleModel.cs index 90c710da26..86da4ce803 100644 --- a/source/backend/keycloak/Models/RoleModel.cs +++ b/source/backend/keycloak/Models/RoleModel.cs @@ -15,7 +15,7 @@ public class RoleModel /// /// get/set - whether or not this role is a composite role. /// - public bool Composite { get; set; } + public bool? Composite { get; set; } #endregion } } diff --git a/source/backend/keycloak/Partials/KeycloakServiceUsers.cs b/source/backend/keycloak/Partials/KeycloakServiceUsers.cs deleted file mode 100644 index 2a65acde05..0000000000 --- a/source/backend/keycloak/Partials/KeycloakServiceUsers.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Pims.Core.Extensions; -using Pims.Keycloak.Extensions; -using Pims.Keycloak.Models; - -namespace Pims.Keycloak -{ - /// - /// KeycloakService class, provides a service for sending HTTP requests to the keycloak admin API. - /// - https://api.loginproxy.gov.bc.ca/openapi/swagger#/ . - /// - public partial class KeycloakService : IKeycloakService - { - #region Methods - - /// - /// Get the user for the specified 'id'. - /// - /// - /// - public async Task GetUserAsync(Guid id) - { - var response = await _client.GetAsync($"{this.Options.ServiceAccount.Api}/{this.Options.ServiceAccount.Environment}/idir/users?guid={id.ToString().Replace("-", string.Empty)}"); - var result = await response.HandleResponseAsync>(); - - return result.Data.FirstOrDefault(); - } - - /// - /// Get an array of the groups the user for the specified 'id' is a member of. - /// - /// - /// - public async Task GetUserGroupsAsync(Guid id) - { - var response = await _client.GetAsync($"{this.Options.ServiceAccount.Api}/integrations/{this.Options.ServiceAccount.Integration}/{this.Options.ServiceAccount.Environment}/user-role-mappings/?username={id.ToString().Replace("-", string.Empty)}@idir"); - - var userRoleModel = await response.HandleResponseAsync(); - - return userRoleModel.Roles.Where(r => r.Composite).ToArray(); - } - - /// - /// Get the total number of groups the user for the specified 'id' is a member of. - /// - /// - /// - public async Task GetUserGroupCountAsync(Guid id) - { - var response = await GetUserGroupsAsync(id); - return response.Length; - } - - /// - /// execute all passed operations. - /// - /// - /// - public async Task ModifyUserRoleMappings(IEnumerable operations) - { - foreach (UserRoleOperation operation in operations) - { - var json = operation.Serialize(); - using var content = new StringContent(json, Encoding.UTF8, "application/json"); - var response = await _client.PostAsync($"{this.Options.ServiceAccount.Api}/integrations/{this.Options.ServiceAccount.Integration}/{this.Options.ServiceAccount.Environment}/user-role-mappings", content); - await response.HandleResponseAsync(); - } - } - #endregion - } -} diff --git a/source/backend/tests/unit/dal/Libraries/Keycloak/KeycloakServiceTest.cs b/source/backend/tests/unit/dal/Libraries/Keycloak/KeycloakServiceTest.cs index 51f7a8fd8c..13cfd43163 100644 --- a/source/backend/tests/unit/dal/Libraries/Keycloak/KeycloakServiceTest.cs +++ b/source/backend/tests/unit/dal/Libraries/Keycloak/KeycloakServiceTest.cs @@ -37,7 +37,7 @@ public void CreateKeycloakService_NoAuthority() // Act // Assert - var result = Assert.Throws(() => helper.Create(options, user)); + var result = Assert.Throws(() => helper.Create(options, user)); result.Message.Should().Be("The configuration for Keycloak:Authority is invalid or missing."); } @@ -60,7 +60,7 @@ public void CreateKeycloakService_NoAudience() // Act // Assert - var result = Assert.Throws(() => helper.Create(options, user)); + var result = Assert.Throws(() => helper.Create(options, user)); result.Message.Should().Be("The configuration for Keycloak:Audience is invalid or missing."); } @@ -84,7 +84,7 @@ public void CreateKeycloakService_NoClient() // Act // Assert - var result = Assert.Throws(() => helper.Create(options, user)); + var result = Assert.Throws(() => helper.Create(options, user)); result.Message.Should().Be("The configuration for Keycloak:Client is invalid or missing."); } @@ -109,7 +109,7 @@ public void CreateKeycloakService_NoServiceAccount() // Act // Assert - var result = Assert.Throws(() => helper.Create(options, user)); + var result = Assert.Throws(() => helper.Create(options, user)); result.Message.Should().Be("The configuration for Keycloak:ServiceAccount is invalid or missing."); } @@ -135,7 +135,7 @@ public void CreateKeycloakService_NoServiceAccountClient() // Act // Assert - var result = Assert.Throws(() => helper.Create(options, user)); + var result = Assert.Throws(() => helper.Create(options, user)); result.Message.Should().Be("The configuration for Keycloak:ServiceAccount:Client is invalid or missing."); } @@ -164,7 +164,7 @@ public void CreateKeycloakService_NoServiceAccountSecret() // Act // Assert - var result = Assert.Throws(() => helper.Create(options, user)); + var result = Assert.Throws(() => helper.Create(options, user)); result.Message.Should().Be("The configuration for Keycloak:ServiceAccount:Secret is invalid or missing."); } @@ -196,7 +196,7 @@ public void CreateKeycloakService() helper.AddSingleton(openIdConnect.Object); // Act - var service = helper.Create(options, user); + var service = helper.Create(options, user); // Assert openIdConnect.Object.AuthClientOptions.Audience.Should().Be(options.Value.Audience); @@ -235,7 +235,7 @@ public void CreateKeycloakService_ServiceAccount() helper.AddSingleton(openIdConnect.Object); // Act - var service = helper.Create(options, user); + var service = helper.Create(options, user); // Assert openIdConnect.Object.AuthClientOptions.Audience.Should().Be(options.Value.ServiceAccount.Audience); diff --git a/source/backend/tests/unit/dal/Libraries/Keycloak/PimsKeycloakUserServiceTest.cs b/source/backend/tests/unit/dal/Libraries/Keycloak/PimsKeycloakUserServiceTest.cs index 84c5a8dd8f..699664fe5d 100644 --- a/source/backend/tests/unit/dal/Libraries/Keycloak/PimsKeycloakUserServiceTest.cs +++ b/source/backend/tests/unit/dal/Libraries/Keycloak/PimsKeycloakUserServiceTest.cs @@ -41,7 +41,7 @@ public async Task UpdateUserAsync_Success() removeRole.RoleId = 1; removeRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var kuser = new Pims.Keycloak.Models.UserModel() { Username = euser.BusinessIdentifierValue, @@ -112,7 +112,7 @@ public async Task UpdateUserAsync_Success_KeycloakRoleNotInPims() removeRole.RoleId = 1; removeRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new string[] { Guid.NewGuid().ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -180,7 +180,7 @@ public async Task UpdateUserAsync_MissingKeycloakUser() var euser = EntityHelper.CreateUser("test"); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); keycloakServiceMock.Setup(m => m.GetUserAsync(It.IsAny())).ReturnsAsync((Pims.Keycloak.Models.UserModel)null); // Act @@ -204,7 +204,7 @@ public async Task UpdateUserAsync_KeycloakUserDoesNotMatch() removeRole.RoleId = 1; removeRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new[] { euser.GuidIdentifierValue.Value.ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -254,7 +254,7 @@ public async Task UpdateUserAsync_AddRoleDoesNotExistInPims() var removeRole = euser.GetRoles().First(); removeRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new string[] { removeRole.KeycloakGroupId.ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -300,7 +300,7 @@ public async Task UpdateUserAsync_RemoveRoleDoesNotExistInPims() removeRole.RoleId = 1; removeRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new string[] { removeRole.KeycloakGroupId.ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -346,7 +346,7 @@ public async Task AppendToUserAsync_Success_AddRole() existingRole.RoleId = 1; existingRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new string[] { existingRole.KeycloakGroupId.ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -406,7 +406,7 @@ public async Task AppendToUserAsync_Success_AddContactMethod() existingRole.RoleId = 1; existingRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new string[] { existingRole.KeycloakGroupId.ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -453,7 +453,7 @@ public async Task AppendToUserAsync_AddRole_KeycloakGroupIdNotFound() existingRole.RoleId = 1; existingRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new string[] { existingRole.KeycloakGroupId.ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -489,7 +489,7 @@ public async Task AppendToUserAsync_AddRole_KeyNotFound() existingRole.RoleId = 1; existingRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new string[] { existingRole.KeycloakGroupId.ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -525,7 +525,7 @@ public async Task UpdateAccessRequestAsync_Recieved() var eRole = EntityHelper.CreateRole("test-role"); eRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new string[] { eRole.KeycloakGroupId.ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -575,7 +575,7 @@ public async Task UpdateAccessRequestAsync_Approved() var eRole = EntityHelper.CreateRole("test-role"); eRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new string[] { eRole.KeycloakGroupId.ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -628,7 +628,7 @@ public async Task UpdateAccessRequestAsync_Approved_RegionUpdate() var eRole = EntityHelper.CreateRole("test-role"); eRole.KeycloakGroupId = Guid.NewGuid(); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var groups = new string[] { eRole.KeycloakGroupId.ToString() }; var kuser = new Pims.Keycloak.Models.UserModel() { @@ -687,7 +687,7 @@ public async Task UpdateAccessRequestAsync_NotAuthorized() var eAccessRequest = EntityHelper.CreateAccessRequest(1); - var keycloakServiceMock = helper.GetMock(); + var keycloakServiceMock = helper.GetMock(); var updatedAccessRequest = new Entity.PimsAccessRequest() { AccessRequestId = eAccessRequest.AccessRequestId, diff --git a/source/backend/tests/unit/dal/Libraries/Keycloak/ServiceCollectionExtensionsTest.cs b/source/backend/tests/unit/dal/Libraries/Keycloak/ServiceCollectionExtensionsTest.cs index 100160fe95..1c0dab6d6b 100644 --- a/source/backend/tests/unit/dal/Libraries/Keycloak/ServiceCollectionExtensionsTest.cs +++ b/source/backend/tests/unit/dal/Libraries/Keycloak/ServiceCollectionExtensionsTest.cs @@ -51,9 +51,9 @@ public void AddKeycloakService_Success() services.AddScoped((s) => mockOpenIdConnectRequestClient.Object); // Act - var result = services.AddKeycloakService(); + var result = services.AddKeycloakRepository(); var provider = result.BuildServiceProvider(); - var service = provider.GetService(); + var service = provider.GetService(); // Assert result.Should().NotBeNull(); @@ -63,7 +63,7 @@ public void AddKeycloakService_Success() } [Fact] - public void AddPimsKeycloakService_Success() + public void AddPimsKeycloakRepository_Success() { // Arrange var services = new ServiceCollection(); diff --git a/tools/core/Keycloak/Configuration/KeycloakAdminOptions.cs b/tools/core/Keycloak/Configuration/KeycloakAdminOptions.cs deleted file mode 100644 index e89b85dd51..0000000000 --- a/tools/core/Keycloak/Configuration/KeycloakAdminOptions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Pims.Core.Http.Configuration; - -namespace Pims.Tools.Core.Keycloak.Configuration -{ - /// - /// KeycloakManagementOptions class, provides a way to configure the connection to Keycloak. - /// - public class KeycloakManagementOptions : AuthClientOptions - { - #region Properties - - /// - /// get/set - The keycloak api route. - /// - public string Api { get; set; } - - /// - /// get/set - The api environment. - /// - public string Environment { get; set; } - - /// - /// get/set - The integration id. - /// - public int Integration { get; set; } - #endregion - } -} diff --git a/tools/core/Keycloak/Configuration/KeycloakOptions.cs b/tools/core/Keycloak/Configuration/KeycloakOptions.cs deleted file mode 100644 index 1ac1122424..0000000000 --- a/tools/core/Keycloak/Configuration/KeycloakOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Pims.Core.Http.Configuration; - -namespace Pims.Tools.Core.Keycloak.Configuration -{ - /// - /// KeycloakOptions class, provides a way to configure the connection to Keycloak. - /// - public class KeycloakOptions : AuthClientOptions - { - #region Properties - - /// - /// get/set - The keycloak realm. - /// - public string Realm { get; set; } - #endregion - } -} diff --git a/tools/core/Keycloak/IKeycloakRequestClient.cs b/tools/core/Keycloak/IKeycloakRequestClient.cs deleted file mode 100644 index 05536af63c..0000000000 --- a/tools/core/Keycloak/IKeycloakRequestClient.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Pims.Tools.Core.Keycloak -{ - /// - /// IRequestClient interface, provides an HTTP client to make requests and handle refresh token. - /// - public interface IKeycloakRequestClient : IRequestClient - { - public string GetIntegrationEnvUri(); - - public string GetIntegrationUri(); - - public string GetEnvUri(); - } -} diff --git a/tools/core/Keycloak/KeycloakRequestClient.cs b/tools/core/Keycloak/KeycloakRequestClient.cs deleted file mode 100644 index 6780af833f..0000000000 --- a/tools/core/Keycloak/KeycloakRequestClient.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Pims.Core.Http.Configuration; -using Pims.Tools.Core.Configuration; -using Pims.Tools.Core.Keycloak.Configuration; - -namespace Pims.Tools.Core.Keycloak -{ - - /// - /// KeycloakRequestClient class, provides a way to make HTTP requests to the keycloak management api provided by sso gold. - /// - public class KeycloakRequestClient : RequestClient, IKeycloakRequestClient - { - #region Variables - private readonly KeycloakManagementOptions _keycloakManagementOptions; - #endregion - - #region Constructors - - /// - /// Creates a new instance of an KeycloakRequestClient class, initializes it with the specified arguments. - /// - /// - /// - /// - /// - /// - /// - /// - public KeycloakRequestClient( - IHttpClientFactory clientFactory, - JwtSecurityTokenHandler tokenHandler, - IOptionsMonitor keycloakManagementOptions, - IOptionsMonitor openIdConnectOptions, - IOptionsMonitor requestOptions, - IOptionsMonitor serializerOptions, - ILogger logger) - : base(clientFactory, tokenHandler, keycloakManagementOptions, openIdConnectOptions, requestOptions, serializerOptions, logger) - { - this.OpenIdConnectOptions.Token = $"{keycloakManagementOptions.CurrentValue.Authority}{this.OpenIdConnectOptions.Token}"; - _keycloakManagementOptions = keycloakManagementOptions.CurrentValue; - } - #endregion - - #region Methods - - // Override the provided url with the base url provided with for the keycloak management api. - public override Task SendAsync(string url, HttpMethod method = null, HttpContent content = null) - { - return base.SendAsync($"{_keycloakManagementOptions.Api}/{url}", method, content); - } - - public override Task SendJsonAsync(string url, HttpMethod method = null, T data = null) - { - return base.SendJsonAsync($"{_keycloakManagementOptions.Api}/{url}", method, data); - } - - // Get the integrations endpoint for a specific environment provided by the appsettings file. - public string GetIntegrationEnvUri() - { - return $"integrations/{_keycloakManagementOptions.Integration}/{_keycloakManagementOptions.Environment}"; - } - - // Get just the integration endpoint provided by the appsettings file. - public string GetIntegrationUri() - { - return $"integrations/{_keycloakManagementOptions.Integration}"; - } - - // Get just the environment from the appsettings file. - public string GetEnvUri() - { - return $"{_keycloakManagementOptions.Environment}"; - } - #endregion - } -} diff --git a/tools/core/Keycloak/Models/ClientModel.cs b/tools/core/Keycloak/Models/ClientModel.cs deleted file mode 100644 index c21eb8cd97..0000000000 --- a/tools/core/Keycloak/Models/ClientModel.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Pims.Tools.Core.Keycloak.Models -{ - /// - /// ClientModel class, provides a model to represent a keycloak client. - /// - public class ClientModel - { - #region Properties - - /// - /// get/set - A unique primary key id. - /// - public Guid? Id { get; set; } - - /// - /// get/set - A unique key identity for the client. - /// - public string ClientId { get; set; } - - /// - /// get/set - A unique name to identify this client. - /// - public string Name { get; set; } - - /// - /// get/set - Client description. - /// - public string Description { get; set; } - - /// - /// get/set - Whether the client is enabled. - /// - public bool Enabled { get; set; } - - /// - /// get/set - The client protocol. - /// - public string Protocol { get; set; } - - /// - /// get/set - Access configuration. - /// - public Dictionary Access { get; set; } - - /// - /// get/set - Authentication flow binding configuration. - /// - public Dictionary AuthenticationFlowBindingOverrides { get; set; } - - // public ResourceServerModel AuthorizationServicesEnabled { get; set; } - - /// - /// get/set - Client authentictor type. - /// - public string ClientAuthenticatorType { get; set; } - - /// - /// get/set - Whether consent is required. - /// - public bool ConsentRequired { get; set; } - - /// - /// get/set - Default client scopes. - /// - public string[] DefaultClientScopes { get; set; } - - /// - /// get/set - Default roles for new users. - /// - public string[] DefaultRoles { get; set; } - - /// - /// get/set - Frontchannel logout. - /// - public bool FrontchannelLogout { get; set; } - - /// - /// get/set - Whether full scope is allowed. - /// - public bool FullScopeAllowed { get; set; } - - /// - /// get/set - Node regregistration timeout. - /// - public int NodeReRegistrationTimeout { get; set; } = -1; - - /// - /// get/set - Not before. - /// - public int NotBefore { get; set; } - - /// - /// get/set - Optional client scopes. - /// - public string[] OptionalClientScopes { get; set; } - - /// - /// get/set - An array of protocol mappers. - /// - public ProtocolMapperModel[] ProtocolMappers { get; set; } - - /// - /// get/set - Whether this is a public client. - /// - public bool PublicClient { get; set; } - - /// - /// get/set - A dictionary of registered nodes. - /// - public Dictionary RegisteredNodes { get; set; } - - /// - /// get/set - Registration access token. - /// - public string RegistrationAccessToken { get; set; } - - /// - /// get/set - Whether surrogate authentication is required. - /// - public bool SurrogateAuthRequired { get; set; } - - /// - /// get/set - Whether authorization services are enabled. - /// - public bool AuthorizationServicesEnabled { get; set; } - - /// - /// get/set - Whether this client is a bearer only. - /// - public bool BearerOnly { get; set; } - - /// - /// get/set - Whether client allows direct access. - /// - public bool DirectAccessGrantsEnabled { get; set; } - - /// - /// get/set - Whether client allows implicit flow. - /// - public bool ImplicitFlowEnabled { get; set; } - - /// - /// get/set - Whether client has a service account. - /// - public bool ServiceAccountsEnabled { get; set; } - - /// - /// get/set - Whether client has standard flow. - /// - public bool StandardFlowEnabled { get; set; } - - /// - /// get/set - Client secret. - /// - public string Secret { get; set; } - - /// - /// get/set - Client base URL. - /// - public string BaseUrl { get; set; } - - /// - /// get/set - Client root URL. - /// - public string RootUrl { get; set; } - - /// - /// get/set - Client redirect URIs. - /// - public string[] RedirectUris { get; set; } - - /// - /// get/set - Client origin. - /// - public string Origin { get; set; } - - /// - /// get/set - Client web origins. - /// - public string[] WebOrigins { get; set; } - - /// - /// get/set - Client admin URL. - /// - public string AdminUrl { get; set; } - - /// - /// get/set - Dictionary of attributes. - /// - public Dictionary Attributes { get; set; } - #endregion - - #region Constructors - - /// - /// Creates a new instance of a ClientModel class. - /// - public ClientModel() - { - } - #endregion - } -} diff --git a/tools/core/Keycloak/Models/GroupModel.cs b/tools/core/Keycloak/Models/GroupModel.cs deleted file mode 100644 index 9e07adcf6b..0000000000 --- a/tools/core/Keycloak/Models/GroupModel.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Pims.Tools.Core.Keycloak.Models -{ - /// - /// GroupModel class, provides a model to represent a keycloak group. - /// - public class GroupModel - { - #region Properties - - /// - /// get/set - A unique primary key. - /// - public Guid? Id { get; set; } - - /// - /// get/set - A unique name to identify this group. - /// - public string Name { get; set; } - - /// - /// get/set - The full path to the group. - /// - public string Path { get; set; } - - /// - /// get/set - An array of role names associated to this group. - /// - public string[] RealmRoles { get; set; } - - /// - /// get/set - A dictionary of client roles. - /// - public Dictionary ClientRoles { get; set; } - - /// - /// get/set - An array of sub-groups. - /// - public string[] SubGroups { get; set; } - - /// - /// get/set - A dictionary of attributes. - /// - public Dictionary Attributes { get; set; } - #endregion - - #region Constructors - - /// - /// Creates a new instance of a GroupModel class. - /// - public GroupModel() - { - } - #endregion - } -} diff --git a/tools/core/Keycloak/Models/ProtocolMapperModel.cs b/tools/core/Keycloak/Models/ProtocolMapperModel.cs deleted file mode 100644 index 3f856218c9..0000000000 --- a/tools/core/Keycloak/Models/ProtocolMapperModel.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; - -namespace Pims.Tools.Core.Keycloak.Models -{ - /// - /// ProtocolMapperModel class, provides a model to represent a keycloak protocol mapper. - /// - public class ProtocolMapperModel - { - #region Properties - - /// - /// get/set - A primary key for the protocol mapper. - /// - public string Id { get; set; } - - /// - /// get/set - A unique name to identify the protocol mapper. - /// - public string Name { get; set; } - - /// - /// get/set - A protocol. - /// - public string Protocol { get; set; } - - /// - /// get/set - The protocol mapper. - /// - public string ProtocolMapper { get; set; } - - /// - /// get/set - The protocol mapper configuration. - /// - public Dictionary Config { get; set; } - #endregion - - #region Constructors - - /// - /// Creates a new instance of a ProtocolMapperModel class. - /// - public ProtocolMapperModel() - { - } - #endregion - } -} diff --git a/tools/core/Keycloak/Models/ResponseWrapperModel.cs b/tools/core/Keycloak/Models/ResponseWrapperModel.cs deleted file mode 100644 index d644c95aed..0000000000 --- a/tools/core/Keycloak/Models/ResponseWrapperModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Pims.Tools.Core.Keycloak.Models -{ - public class ResponseWrapperModel - { - #region properties - public T Data { get; set; } - #endregion - } -} diff --git a/tools/core/Keycloak/Models/RoleModel.cs b/tools/core/Keycloak/Models/RoleModel.cs deleted file mode 100644 index 6cc55b2086..0000000000 --- a/tools/core/Keycloak/Models/RoleModel.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Pims.Tools.Core.Keycloak.Models -{ - /// - /// RoleModel class, provides a model to represent a keycloak role. - /// - public class RoleModel - { - #region Properties - - /// - /// get/set - A unique name to identify the role. - /// - public string Name { get; set; } - - /// - /// get/set - true if this role has sub-roles. - /// - public bool? Composite { get; set; } - #endregion - - #region Constructors - - /// - /// Creates a new instance of a RoleModel class. - /// - public RoleModel() - { - } - #endregion - } -} diff --git a/tools/core/Keycloak/Models/UserModel.cs b/tools/core/Keycloak/Models/UserModel.cs deleted file mode 100644 index 986d85e830..0000000000 --- a/tools/core/Keycloak/Models/UserModel.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Pims.Tools.Core.Keycloak.Models -{ - /// - /// UserModel class, provides a model to represent a keycloak user. - /// - public class UserModel - { - #region Properties - - /// - /// get/set - The primary key IDENTITY. - /// - public Guid Id { get; set; } - - /// - /// get/set - A unique username to identify the user. - /// - public string Username { get; set; } - - /// - /// get/set - Whether the user is enabled. - /// - public bool Enabled { get; set; } - - /// - /// get/set - I don't know... - /// - public bool Totp { get; set; } - - /// - /// get/set - Whether the user's email has been verified. - /// - public bool EmailVerified { get; set; } - - /// - /// get/set - The user's first name. - /// - public string FirstName { get; set; } - - /// - /// get/set - The user's last name. - /// - public string LastName { get; set; } - - /// - /// get/set - The user's email. - /// - public string Email { get; set; } - - /// - /// get/set - A dictionary of attributes. - /// - public Dictionary Attributes { get; set; } - #endregion - } -} diff --git a/tools/core/Keycloak/Models/UserRoleModel.cs b/tools/core/Keycloak/Models/UserRoleModel.cs deleted file mode 100644 index 3b42c9e498..0000000000 --- a/tools/core/Keycloak/Models/UserRoleModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace Pims.Tools.Core.Keycloak.Models -{ - /// - /// UserModel class, provides a model to represent a keycloak user. - /// - public class UserRoleModel - { - #region Properties - public IEnumerable Users { get; set; } - - public IEnumerable Roles { get; set; } - #endregion - } -} diff --git a/tools/core/Keycloak/Models/UserRoleOperation.cs b/tools/core/Keycloak/Models/UserRoleOperation.cs deleted file mode 100644 index f5f17c1ffa..0000000000 --- a/tools/core/Keycloak/Models/UserRoleOperation.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Pims.Tools.Core.Keycloak.Models -{ - /// - /// UserRoleOperation class, provides a model to represent a keycloak user. - /// - public class UserRoleOperation - { - #region Properties - - /// - /// get/set - A unique role name to identify the role in keycloak. - /// - public string RoleName { get; set; } - - /// - /// get/set - A unique username to identify the user in keycloak. - /// - public string Username { get; set; } - - /// - /// get/set - An operation name, either add or remove. - /// - public string Operation { get; set; } - #endregion - } -} diff --git a/tools/core/Pims.Tools.Core.csproj b/tools/core/Pims.Tools.Core.csproj deleted file mode 100644 index 8971335e88..0000000000 --- a/tools/core/Pims.Tools.Core.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - library - net6.0 - 1.0.0.0 - 1.0.0.0 - 561C2601-E1DB-44DE-B772-518A6FEA63D9 - Debug;Release - - - - - - - - - - - - - - - - - - - - diff --git a/tools/core/IRequestClient.cs b/tools/keycloak/sync/Client/IRequestClient.cs similarity index 97% rename from tools/core/IRequestClient.cs rename to tools/keycloak/sync/Client/IRequestClient.cs index 98e958df07..e945efcf68 100644 --- a/tools/core/IRequestClient.cs +++ b/tools/keycloak/sync/Client/IRequestClient.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Pims.Core.Http; -namespace Pims.Tools.Core +namespace Pims.Tools.Keycloak.Sync { /// /// IRequestClient interface, provides an HTTP client to make requests and handle refresh token. diff --git a/tools/keycloak/sync/Client/PimsRequestClient.cs b/tools/keycloak/sync/Client/PimsRequestClient.cs index 99cc5d85d6..2df6c4abb0 100644 --- a/tools/keycloak/sync/Client/PimsRequestClient.cs +++ b/tools/keycloak/sync/Client/PimsRequestClient.cs @@ -5,9 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Pims.Core.Http.Configuration; -using Pims.Tools.Core; -using Pims.Tools.Core.Configuration; -using Pims.Tools.Core.Keycloak.Configuration; +using Pims.Keycloak.Configuration; using Pims.Tools.Keycloak.Sync.Configuration; namespace Pims.Tools.Keycloak.Sync diff --git a/tools/core/RequestClient.cs b/tools/keycloak/sync/Client/RequestClient.cs similarity index 98% rename from tools/core/RequestClient.cs rename to tools/keycloak/sync/Client/RequestClient.cs index 31629bb9d0..31141cbd0a 100644 --- a/tools/core/RequestClient.cs +++ b/tools/keycloak/sync/Client/RequestClient.cs @@ -1,7 +1,6 @@ using System; using System.IdentityModel.Tokens.Jwt; using System.Net.Http; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -9,11 +8,11 @@ using Microsoft.Extensions.Options; using Pims.Core.Exceptions; using Pims.Core.Http.Configuration; -using Pims.Tools.Core.Configuration; +using Pims.Tools.Keycloak.Sync.Configuration; using Polly; using Polly.Retry; -namespace Pims.Tools.Core +namespace Pims.Tools.Keycloak.Sync { /// /// RequestClient class, provides a way to make HTTP requests, handle errors and handle refresh tokens. diff --git a/tools/keycloak/sync/Configuration/AuthOptions.cs b/tools/keycloak/sync/Configuration/AuthOptions.cs index 71bd16d99b..ab98571f7b 100644 --- a/tools/keycloak/sync/Configuration/AuthOptions.cs +++ b/tools/keycloak/sync/Configuration/AuthOptions.cs @@ -1,5 +1,5 @@ using Pims.Core.Http.Configuration; -using Pims.Tools.Core.Keycloak.Configuration; +using Pims.Keycloak.Configuration; namespace Pims.Tools.Keycloak.Sync.Configuration { diff --git a/tools/core/Configuration/RequestOptions.cs b/tools/keycloak/sync/Configuration/RequestOptions.cs similarity index 94% rename from tools/core/Configuration/RequestOptions.cs rename to tools/keycloak/sync/Configuration/RequestOptions.cs index 4ee47f1df5..badedeacb1 100644 --- a/tools/core/Configuration/RequestOptions.cs +++ b/tools/keycloak/sync/Configuration/RequestOptions.cs @@ -1,4 +1,4 @@ -namespace Pims.Tools.Core.Configuration +namespace Pims.Tools.Keycloak.Sync.Configuration { /// /// RequestOptions class, provides a way to configure requests. diff --git a/tools/keycloak/sync/Configuration/ToolOptions.cs b/tools/keycloak/sync/Configuration/ToolOptions.cs index 6afcc5166d..4e41f14099 100644 --- a/tools/keycloak/sync/Configuration/ToolOptions.cs +++ b/tools/keycloak/sync/Configuration/ToolOptions.cs @@ -1,4 +1,3 @@ -using Pims.Tools.Core.Configuration; using Pims.Tools.Keycloak.Sync.Configuration.Realm; namespace Pims.Tools.Keycloak.Sync.Configuration diff --git a/tools/keycloak/sync/Models/Keycloak/ClientModel.cs b/tools/keycloak/sync/Models/Keycloak/ClientModel.cs index bac2a582fc..78e932ead3 100644 --- a/tools/keycloak/sync/Models/Keycloak/ClientModel.cs +++ b/tools/keycloak/sync/Models/Keycloak/ClientModel.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using Pims.Tools.Keycloak.Sync.Configuration.Realm; @@ -6,9 +8,191 @@ namespace Pims.Tools.Keycloak.Sync.Models.Keycloak /// /// ClientModel class, provides a model to represent a keycloak client. /// - public class ClientModel : Core.Keycloak.Models.ClientModel + public class ClientModel { #region Properties + + /// + /// get/set - A unique primary key id. + /// + public Guid? Id { get; set; } + + /// + /// get/set - A unique key identity for the client. + /// + public string ClientId { get; set; } + + /// + /// get/set - A unique name to identify this client. + /// + public string Name { get; set; } + + /// + /// get/set - Client description. + /// + public string Description { get; set; } + + /// + /// get/set - Whether the client is enabled. + /// + public bool Enabled { get; set; } + + /// + /// get/set - The client protocol. + /// + public string Protocol { get; set; } + + /// + /// get/set - Access configuration. + /// + public Dictionary Access { get; set; } + + /// + /// get/set - Authentication flow binding configuration. + /// + public Dictionary AuthenticationFlowBindingOverrides { get; set; } + + // public ResourceServerModel AuthorizationServicesEnabled { get; set; } + + /// + /// get/set - Client authentictor type. + /// + public string ClientAuthenticatorType { get; set; } + + /// + /// get/set - Whether consent is required. + /// + public bool ConsentRequired { get; set; } + + /// + /// get/set - Default client scopes. + /// + public string[] DefaultClientScopes { get; set; } + + /// + /// get/set - Default roles for new users. + /// + public string[] DefaultRoles { get; set; } + + /// + /// get/set - Frontchannel logout. + /// + public bool FrontchannelLogout { get; set; } + + /// + /// get/set - Whether full scope is allowed. + /// + public bool FullScopeAllowed { get; set; } + + /// + /// get/set - Node regregistration timeout. + /// + public int NodeReRegistrationTimeout { get; set; } = -1; + + /// + /// get/set - Not before. + /// + public int NotBefore { get; set; } + + /// + /// get/set - Optional client scopes. + /// + public string[] OptionalClientScopes { get; set; } + + /// + /// get/set - An array of protocol mappers. + /// + public ProtocolMapperModel[] ProtocolMappers { get; set; } + + /// + /// get/set - Whether this is a public client. + /// + public bool PublicClient { get; set; } + + /// + /// get/set - A dictionary of registered nodes. + /// + public Dictionary RegisteredNodes { get; set; } + + /// + /// get/set - Registration access token. + /// + public string RegistrationAccessToken { get; set; } + + /// + /// get/set - Whether surrogate authentication is required. + /// + public bool SurrogateAuthRequired { get; set; } + + /// + /// get/set - Whether authorization services are enabled. + /// + public bool AuthorizationServicesEnabled { get; set; } + + /// + /// get/set - Whether this client is a bearer only. + /// + public bool BearerOnly { get; set; } + + /// + /// get/set - Whether client allows direct access. + /// + public bool DirectAccessGrantsEnabled { get; set; } + + /// + /// get/set - Whether client allows implicit flow. + /// + public bool ImplicitFlowEnabled { get; set; } + + /// + /// get/set - Whether client has a service account. + /// + public bool ServiceAccountsEnabled { get; set; } + + /// + /// get/set - Whether client has standard flow. + /// + public bool StandardFlowEnabled { get; set; } + + /// + /// get/set - Client secret. + /// + public string Secret { get; set; } + + /// + /// get/set - Client base URL. + /// + public string BaseUrl { get; set; } + + /// + /// get/set - Client root URL. + /// + public string RootUrl { get; set; } + + /// + /// get/set - Client redirect URIs. + /// + public string[] RedirectUris { get; set; } + + /// + /// get/set - Client origin. + /// + public string Origin { get; set; } + + /// + /// get/set - Client web origins. + /// + public string[] WebOrigins { get; set; } + + /// + /// get/set - Client admin URL. + /// + public string AdminUrl { get; set; } + + /// + /// get/set - Dictionary of attributes. + /// + public Dictionary Attributes { get; set; } #endregion #region Constructors diff --git a/tools/keycloak/sync/Models/Keycloak/GroupModel.cs b/tools/keycloak/sync/Models/Keycloak/GroupModel.cs index 875b00b773..526d53d448 100644 --- a/tools/keycloak/sync/Models/Keycloak/GroupModel.cs +++ b/tools/keycloak/sync/Models/Keycloak/GroupModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Pims.Tools.Keycloak.Sync.Configuration.Realm; @@ -7,9 +8,45 @@ namespace Pims.Tools.Keycloak.Sync.Models.Keycloak /// /// GroupModel class, provides a model to represent a keycloak group. /// - public class GroupModel : Core.Keycloak.Models.GroupModel + public class GroupModel { #region Properties + + /// + /// get/set - A unique primary key. + /// + public Guid? Id { get; set; } + + /// + /// get/set - A unique name to identify this group. + /// + public string Name { get; set; } + + /// + /// get/set - The full path to the group. + /// + public string Path { get; set; } + + /// + /// get/set - An array of role names associated to this group. + /// + public string[] RealmRoles { get; set; } + + /// + /// get/set - A dictionary of client roles. + /// + public Dictionary ClientRoles { get; set; } + + /// + /// get/set - An array of sub-groups. + /// + public string[] SubGroups { get; set; } + + /// + /// get/set - A dictionary of attributes. + /// + public Dictionary Attributes { get; set; } + #endregion #region Constructors diff --git a/tools/keycloak/sync/Models/Keycloak/ProtocolMapperModel.cs b/tools/keycloak/sync/Models/Keycloak/ProtocolMapperModel.cs index 965f4680e4..e35b77c51c 100644 --- a/tools/keycloak/sync/Models/Keycloak/ProtocolMapperModel.cs +++ b/tools/keycloak/sync/Models/Keycloak/ProtocolMapperModel.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Pims.Tools.Keycloak.Sync.Configuration.Realm; namespace Pims.Tools.Keycloak.Sync.Models.Keycloak @@ -5,9 +6,35 @@ namespace Pims.Tools.Keycloak.Sync.Models.Keycloak /// /// ProtocolMapperModel class, provides a model to represent a keycloak protocol mapper. /// - public class ProtocolMapperModel : Core.Keycloak.Models.ProtocolMapperModel + public class ProtocolMapperModel { #region Properties + + /// + /// get/set - A primary key for the protocol mapper. + /// + public string Id { get; set; } + + /// + /// get/set - A unique name to identify the protocol mapper. + /// + public string Name { get; set; } + + /// + /// get/set - A protocol. + /// + public string Protocol { get; set; } + + /// + /// get/set - The protocol mapper. + /// + public string ProtocolMapper { get; set; } + + /// + /// get/set - The protocol mapper configuration. + /// + public Dictionary Config { get; set; } + #endregion #region Constructors diff --git a/tools/core/Keycloak/Models/RealmModel.cs b/tools/keycloak/sync/Models/Keycloak/RealmModel.cs similarity index 100% rename from tools/core/Keycloak/Models/RealmModel.cs rename to tools/keycloak/sync/Models/Keycloak/RealmModel.cs diff --git a/tools/keycloak/sync/Models/Keycloak/RoleModel.cs b/tools/keycloak/sync/Models/Keycloak/RoleModel.cs deleted file mode 100644 index 0f6c65456d..0000000000 --- a/tools/keycloak/sync/Models/Keycloak/RoleModel.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Pims.Tools.Keycloak.Sync.Configuration.Realm; - -namespace Pims.Tools.Keycloak.Sync.Models.Keycloak -{ - /// - /// RoleModel class, provides a model to represent a keycloak role. - /// - public class RoleModel : Core.Keycloak.Models.RoleModel - { - #region Properties - #endregion - - #region Constructors - - /// - /// Creates a new instance of a RoleModel class. - /// - public RoleModel() - { - } - - /// - /// Creates a new instance of a RoleModel class, initializes with specified arguments. - /// - /// - public RoleModel(RoleOptions role) - { - Name = role.Name; - } - #endregion - } -} diff --git a/tools/keycloak/sync/Pims.Tools.Keycloak.Sync.csproj b/tools/keycloak/sync/Pims.Tools.Keycloak.Sync.csproj index 6b9a8395d8..c097c5105a 100644 --- a/tools/keycloak/sync/Pims.Tools.Keycloak.Sync.csproj +++ b/tools/keycloak/sync/Pims.Tools.Keycloak.Sync.csproj @@ -36,6 +36,7 @@ + @@ -59,7 +60,6 @@ - diff --git a/tools/keycloak/sync/Pims.Tools.Keycloak.Sync.sln b/tools/keycloak/sync/Pims.Tools.Keycloak.Sync.sln index c599a0a3c1..be746525eb 100644 --- a/tools/keycloak/sync/Pims.Tools.Keycloak.Sync.sln +++ b/tools/keycloak/sync/Pims.Tools.Keycloak.Sync.sln @@ -8,12 +8,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Tools.Keycloak.Sync", {16BC0468-78F6-4C91-87DA-7403C919E646} = {16BC0468-78F6-4C91-87DA-7403C919E646} EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Tools.Core", "..\..\core\Pims.Tools.Core.csproj", "{561C2601-E1DB-44DE-B772-518A6FEA63D9}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Core", "..\..\..\source\backend\core\Pims.Core.csproj", "{AC8F04FF-3164-41FB-9EDF-E468B8B77837}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Api", "..\..\..\source\backend\api\Pims.Api.csproj", "{16BC0468-78F6-4C91-87DA-7403C919E646}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Keycloak", "..\..\..\source\backend\keycloak\Pims.Keycloak.csproj", "{970903E9-BC53-436F-BA77-C62349546425}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,10 +24,6 @@ Global {951EEA85-8BCA-4D88-9CA5-0453F447DFC5}.Debug|Any CPU.Build.0 = Debug|Any CPU {951EEA85-8BCA-4D88-9CA5-0453F447DFC5}.Release|Any CPU.ActiveCfg = Release|Any CPU {951EEA85-8BCA-4D88-9CA5-0453F447DFC5}.Release|Any CPU.Build.0 = Release|Any CPU - {561C2601-E1DB-44DE-B772-518A6FEA63D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {561C2601-E1DB-44DE-B772-518A6FEA63D9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {561C2601-E1DB-44DE-B772-518A6FEA63D9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {561C2601-E1DB-44DE-B772-518A6FEA63D9}.Release|Any CPU.Build.0 = Release|Any CPU {AC8F04FF-3164-41FB-9EDF-E468B8B77837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AC8F04FF-3164-41FB-9EDF-E468B8B77837}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC8F04FF-3164-41FB-9EDF-E468B8B77837}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/tools/keycloak/sync/Program.cs b/tools/keycloak/sync/Program.cs index 81564c8a10..270437286b 100644 --- a/tools/keycloak/sync/Program.cs +++ b/tools/keycloak/sync/Program.cs @@ -8,10 +8,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pims.Core.Exceptions; +using Pims.Core.Http; using Pims.Core.Http.Configuration; -using Pims.Tools.Core.Configuration; -using Pims.Tools.Core.Keycloak; -using Pims.Tools.Core.Keycloak.Configuration; +using Pims.Keycloak; +using Pims.Keycloak.Configuration; using Pims.Tools.Keycloak.Sync.Configuration; using Pims.Tools.Keycloak.Sync.Configuration.Realm; @@ -40,7 +40,7 @@ public static async Task Main(string[] args) .Configure(config.GetSection("Api")) .Configure(config.GetSection("Realm")) .Configure(config.GetSection("Auth:Keycloak")) - .Configure(config.GetSection("Auth:Keycloak:ServiceAccount")) + .Configure(config.GetSection("Auth:Keycloak:ServiceAccount")) .Configure(config.GetSection("Auth:OpenIdConnect")) .Configure(options => { @@ -78,8 +78,9 @@ public static async Task Main(string[] args) }) .AddTransient() .AddTransient() - .AddScoped() + .AddScoped() .AddScoped() + .AddScoped() .AddTransient(); services.AddHttpClient("Pims.Tools.Keycloak.Sync", client => { }); diff --git a/tools/keycloak/sync/ISyncFactory.cs b/tools/keycloak/sync/Syncronizer/ISyncFactory.cs similarity index 100% rename from tools/keycloak/sync/ISyncFactory.cs rename to tools/keycloak/sync/Syncronizer/ISyncFactory.cs diff --git a/tools/keycloak/sync/SyncFactory.cs b/tools/keycloak/sync/Syncronizer/SyncFactory.cs similarity index 77% rename from tools/keycloak/sync/SyncFactory.cs rename to tools/keycloak/sync/Syncronizer/SyncFactory.cs index b23a229452..b91c88e093 100644 --- a/tools/keycloak/sync/SyncFactory.cs +++ b/tools/keycloak/sync/Syncronizer/SyncFactory.cs @@ -8,9 +8,8 @@ using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; using Pims.Core.Exceptions; -using Pims.Tools.Core.Keycloak; -using Pims.Tools.Core.Keycloak.Models; -using KModel = Pims.Tools.Core.Keycloak.Models; +using Pims.Keycloak; +using KModel = Pims.Keycloak.Models; using PModel = Pims.Api.Models.Concepts; namespace Pims.Tools.Keycloak.Sync @@ -23,7 +22,7 @@ public class SyncFactory : ISyncFactory { #region Variables private static readonly int MAXPAGES = 20; - private readonly IKeycloakRequestClient _keycloakManagementClient; + private readonly IKeycloakRepository _keycloakRepository; private readonly IPimsRequestClient _pimsClient; private readonly ILogger _logger; #endregion @@ -33,12 +32,12 @@ public class SyncFactory : ISyncFactory /// /// Creates a new instance of an Factory class, initializes it with the specified arguments. /// - /// + /// /// /// - public SyncFactory(IKeycloakRequestClient keycloakClient, IPimsRequestClient pimsClient, ILogger logger) + public SyncFactory(IKeycloakRepository keycloakRepository, IPimsRequestClient pimsClient, ILogger logger) { - _keycloakManagementClient = keycloakClient; + _keycloakRepository = keycloakRepository; _pimsClient = pimsClient; _logger = logger; } @@ -79,7 +78,7 @@ public async Task SyncAsync() /// private async Task ActivateAccountAsync() { - var aRes = await _pimsClient.HandleRequestAsync (HttpMethod.Post, "auth/activate"); + var aRes = await _pimsClient.HandleRequestAsync(HttpMethod.Post, "auth/activate"); if (!aRes.IsSuccessStatusCode) { throw new HttpClientRequestException(aRes); @@ -106,7 +105,7 @@ private async Task SyncClaimsAsync(StringBuilder log, StringBuilder errorLog) } while (claimsPage != null && claimsPage.Items.Any() && page < MAXPAGES); - var allKeycloakRoles = await _keycloakManagementClient.HandleRequestAsync>(HttpMethod.Get, $"{_keycloakManagementClient.GetIntegrationEnvUri()}/roles"); + var allKeycloakRoles = await _keycloakRepository.GetAllRoles(); var keycloakRoles = allKeycloakRoles.Data.Where(x => x.Composite.HasValue && x.Composite.Value == false); var keycloakRolesToDelete = keycloakRoles.Where(r => claims.All(crr => crr.Name != r.Name)); @@ -146,10 +145,10 @@ private async Task AddKeycloakRoleAsync(PModel.ClaimModel claim) // Add the role to keycloak and sync with PIMS. _logger.LogInformation($"Adding keycloak role: {claim.Name}"); - var kresponse = await _keycloakManagementClient.SendJsonAsync($"{_keycloakManagementClient.GetIntegrationEnvUri()}/roles", HttpMethod.Post, krole); - if (kresponse.StatusCode != HttpStatusCode.Created) + var response = await _keycloakRepository.AddKeycloakRole(krole); + if (response.StatusCode != HttpStatusCode.Created) { - throw new HttpClientRequestException(kresponse, $"Failed to add the role '{claim.Name}' to keycloak."); + throw new HttpClientRequestException(response, $"Failed to add the role '{claim.Name}' to keycloak."); } _logger.LogInformation($"Keycloak role: {claim.Name} added"); return; @@ -176,7 +175,7 @@ private async Task SyncRolesAsync(StringBuilder log, StringBuilder errorLog) } while (rolesPage != null && rolesPage.Items.Any() && page < MAXPAGES); - var keycloakRoles = await _keycloakManagementClient.HandleRequestAsync>(HttpMethod.Get, $"{_keycloakManagementClient.GetIntegrationEnvUri()}/roles"); + var keycloakRoles = await _keycloakRepository.GetAllRoles(); var keycloakCompositeRoles = keycloakRoles.Data.Where(x => x.Composite.HasValue && x.Composite.Value); var keycloakRolesToDelete = keycloakCompositeRoles.Where(r => roles.All(crr => crr.Name != r.Name)); @@ -187,7 +186,7 @@ private async Task SyncRolesAsync(StringBuilder log, StringBuilder errorLog) private async Task AddRolesFromPims(IEnumerable roles, StringBuilder log, StringBuilder errorLog) { - var keycloakRoles = await _keycloakManagementClient.HandleRequestAsync>(HttpMethod.Get, $"{_keycloakManagementClient.GetIntegrationEnvUri()}/roles"); + var keycloakRoles = await _keycloakRepository.GetAllRoles(); foreach (var role in roles) { try @@ -197,7 +196,7 @@ private async Task AddRolesFromPims(IEnumerable roles, StringB { continue; } - + _logger.LogInformation($"Adding/updating keycloak composite role '{prole.Name}'"); var matchingKeycloakGroup = keycloakRoles.Data.FirstOrDefault(r => r.Name == prole.Name); if (matchingKeycloakGroup != null) @@ -224,7 +223,7 @@ private async Task RemoveRolesFromPims(IEnumerable roles, Stri try { _logger.LogInformation($"Deleting keycloak role '{role.Name}'"); - await _keycloakManagementClient.DeleteAsync($"{_keycloakManagementClient.GetIntegrationEnvUri()}/roles/{Uri.EscapeDataString(role.Name)}"); + await _keycloakRepository.DeleteRole(role.Name); LogInfo(log, $"Deleted keycloak role '{role.Name}'"); } catch (HttpClientRequestException ex) @@ -243,16 +242,16 @@ private async Task RemoveRolesFromPims(IEnumerable roles, Stri /// private async Task AddGroupToKeycloak(PModel.RoleModel role) { - var addGroup = new KModel.RoleModel() + var krole = new KModel.RoleModel() { Name = role.Name, }; // Add the group to keycloak and sync with PIMS. - var response = await _keycloakManagementClient.SendJsonAsync($"{_keycloakManagementClient.GetIntegrationEnvUri()}/roles", HttpMethod.Post, addGroup); + var response = await _keycloakRepository.AddKeycloakRole(krole); if (response.StatusCode == HttpStatusCode.Created) { - await AddRolesToGroupInKeycloak(addGroup, role); + await AddRolesToGroupInKeycloak(krole, role); } else { @@ -285,12 +284,12 @@ private async Task AddRolesToGroupInKeycloak(KModel.RoleModel group, PModel.Role Name = c.Claim.Name, }).ToArray(); - var allKeycloakGroupRolesResponse = await _keycloakManagementClient.HandleGetAsync>>($"{_keycloakManagementClient.GetIntegrationEnvUri()}/roles/{Uri.EscapeDataString(group.Name)}/composite-roles"); + var allKeycloakGroupRolesResponse = await _keycloakRepository.GetAllGroupRoles(group.Name); var newRolesToAdd = allPimsGroupRoles.Where(r => allKeycloakGroupRolesResponse.Data.All(crr => crr.Name != r.Name)); if (newRolesToAdd.Any()) { _logger.LogInformation($"Adding the following roles to the composite role {role.Name}: {string.Join(',', newRolesToAdd.Select(r => r.Name))}"); - var response = await _keycloakManagementClient.SendJsonAsync($"{_keycloakManagementClient.GetIntegrationEnvUri()}/roles/{Uri.EscapeDataString(group.Name)}/composite-roles", HttpMethod.Post, newRolesToAdd); + var response = await _keycloakRepository.AddKeycloakRolesToGroup(group.Name, newRolesToAdd); _logger.LogInformation($"Added the following roles to the composite role {role.Name}: {string.Join(',', newRolesToAdd.Select(r => r.Name))}"); if (!response.IsSuccessStatusCode) { @@ -312,14 +311,14 @@ private async Task RemoveRolesFromGroupInKeycloak(KModel.RoleModel group, PModel Name = c.Claim.Name, }).ToArray(); - var allKeycloakGroupRolesResponse = await _keycloakManagementClient.HandleGetAsync>>($"{_keycloakManagementClient.GetIntegrationEnvUri()}/roles/{Uri.EscapeDataString(group.Name)}/composite-roles"); + var allKeycloakGroupRolesResponse = await _keycloakRepository.GetAllGroupRoles(group.Name); var rolesToRemove = allKeycloakGroupRolesResponse.Data.Where(r => allPimsGroupRoles.All(crr => crr.Name != r.Name)); foreach (var roleToRemove in rolesToRemove) { _logger.LogInformation($"Deleting the following roles to the composite role {role.Name}: {roleToRemove}"); // Update the group in keycloak. - var response = await _keycloakManagementClient.DeleteAsync($"{_keycloakManagementClient.GetIntegrationEnvUri()}/roles/{Uri.EscapeDataString(group.Name)}/composite-roles/{Uri.EscapeDataString(roleToRemove.Name)}"); + var response = await _keycloakRepository.DeleteRoleFromGroup(group.Name, roleToRemove.Name); _logger.LogInformation($"Deleting the following roles to the composite role {role.Name}: {roleToRemove}"); if (!response.IsSuccessStatusCode) { @@ -383,32 +382,33 @@ private async Task SyncUsersAsync(StringBuilder log, StringBuilder errorLog) private async Task SyncUserRoles(PModel.UserModel user, StringBuilder log) { var username = user.GuidIdentifierValue.ToString().Replace("-", string.Empty) + "@idir"; - var kUserRoles = (await _keycloakManagementClient.HandleGetAsync>>( - $"{_keycloakManagementClient.GetIntegrationEnvUri()}/users/{Uri.EscapeDataString(username)}/roles", - (response) => response.StatusCode == HttpStatusCode.NotFound)).Data; + var response = await _keycloakRepository.GetUserRoles(username); + var kUserRoles = response.Data; - IEnumerable userRolesToAdd = user.UserRoles.Where(ur => kUserRoles.All(kr => kr.Name != ur.Role.Name && ur.Role.IsDisabled == false)).Select(ur => new RoleModel() { Name = ur.Role.Name }); - IEnumerable userRolesToRemove = kUserRoles.Where(kur => user.UserRoles.All(ur => ur.Role.Name != kur.Name)).Select(ur => new RoleModel() { Name = ur.Name }); + IEnumerable userRolesToAdd = user.UserRoles.Where(ur => kUserRoles.All(kr => kr.Name != ur.Role.Name && ur.Role.IsDisabled == false)) + .Select(ur => new KModel.RoleModel() { Name = ur.Role.Name }); + IEnumerable userRolesToRemove = kUserRoles.Where(kur => user.UserRoles.All(ur => ur.Role.Name != kur.Name)) + .Select(ur => new KModel.RoleModel() { Name = ur.Name }); if (userRolesToAdd.Any()) { _logger.LogInformation($"Executing operation 'add' on roles '{string.Join(',', userRolesToAdd.Select(r => r.Name))}' to user '{username}'"); - var response = await _keycloakManagementClient.SendJsonAsync($"{_keycloakManagementClient.GetIntegrationEnvUri()}/users/{Uri.EscapeDataString(username)}/roles", HttpMethod.Post, userRolesToAdd); - if (!response.IsSuccessStatusCode) + var addResponse = await _keycloakRepository.AddRolesToUser(username, userRolesToAdd); + if (!addResponse.IsSuccessStatusCode) { - throw new HttpClientRequestException(response, $"Failed to update the user role mappings for '{username}' during operation 'add' on roles '{string.Join(',', userRolesToAdd.Select(r => r.Name))}'"); + throw new HttpClientRequestException(addResponse, $"Failed to update the user role mappings for '{username}' during operation 'add' on roles '{string.Join(',', userRolesToAdd.Select(r => r.Name))}'"); } LogInfo(log, $"Executed operation 'add' on roles '{string.Join(',', userRolesToAdd.Select(r => r.Name))}' to user '{username}'"); } - foreach (RoleModel userRoleToRemove in userRolesToRemove) + foreach (var userRoleToRemove in userRolesToRemove) { _logger.LogInformation($"Executing operation 'delete' on role '{userRoleToRemove.Name}' to user '{username}'"); - var response = await _keycloakManagementClient.SendAsync($"{_keycloakManagementClient.GetIntegrationEnvUri()}/users/{Uri.EscapeDataString(username)}/roles/{Uri.EscapeDataString(userRoleToRemove.Name)}", HttpMethod.Delete); - if (!response.IsSuccessStatusCode) + var deleteResponse = await _keycloakRepository.DeleteRoleFromUsers(username, userRoleToRemove.Name); + if (!deleteResponse.IsSuccessStatusCode) { - throw new HttpClientRequestException(response, $"Failed to update the user role mappings for '{username}' during operation 'delete' on role '{userRoleToRemove.Name}'"); + throw new HttpClientRequestException(deleteResponse, $"Failed to update the user role mappings for '{username}' during operation 'delete' on role '{userRoleToRemove.Name}'"); } LogInfo(log, $"Executed operation 'delete' on role '{userRoleToRemove.Name}' to user '{username}'"); } @@ -425,12 +425,12 @@ private async Task SyncUserRoles(PModel.UserModel user, StringBuilder log) try { // Make a request to keycloak to find a matching user. - var response = await _keycloakManagementClient.HandleRequestAsync>>(HttpMethod.Get, $"{_keycloakManagementClient.GetEnvUri()}/idir/users?guid={user.GuidIdentifierValue.ToString().Replace("-", string.Empty)}"); - if (response.Data.Count() > 1) + var users = await _keycloakRepository.GetUsersAsync(user.GuidIdentifierValue); + if (users.Count() > 1) { throw new HttpClientRequestException($"Found multiple users in keycloak for GUID: user: {user.GuidIdentifierValue.ToString()}"); } - return response.Data.FirstOrDefault(); + return users.FirstOrDefault(); } catch (HttpClientRequestException ex) { From b2db13e04cec99b74a752bdd20ab8b94946fba6c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 21:25:27 +0000 Subject: [PATCH 08/24] CI: Bump version to v4.0.0-67.27 --- source/backend/api/Pims.Api.csproj | 4 ++-- source/frontend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 037d2d1246..b4f44470ea 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,8 +2,8 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 4.0.0-67.26 - 4.0.0-67.26 + 4.0.0-67.27 + 4.0.0-67.27 4.0.0.67 true 16BC0468-78F6-4C91-87DA-7403C919E646 diff --git a/source/frontend/package.json b/source/frontend/package.json index e9d7ccc304..b257711306 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "4.0.0-67.26", + "version": "4.0.0-67.27", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From a83aef47490528adc927a69208f11d9284395397 Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Fri, 1 Dec 2023 13:50:57 -0800 Subject: [PATCH 09/24] System error modal | psp-7304 (#3628) * Updated Generic modal to use a variant * Improved System error modal * Lit fixes * Info fixes --- source/frontend/src/assets/scss/App.scss | 109 -------------- .../src/assets/scss/_variables.module.scss | 73 +++++----- .../common/CancelConfirmationModal.tsx | 6 +- .../src/components/common/ErrorModal.tsx | 1 + .../src/components/common/ErrorTabs.tsx | 1 + .../components/common/GenericModal.test.tsx | 14 +- .../src/components/common/GenericModal.tsx | 135 +++++++++++++++++- .../components/common/ModalContainer.test.tsx | 6 +- .../src/components/common/form/NotesModal.tsx | 2 +- .../components/common/mapFSM/useMapSearch.tsx | 3 +- .../contact/ContactManagerModal.tsx | 2 +- .../components/layout/Header/ErrorModal.tsx | 122 ++++++++++------ .../Header/__snapshots__/Header.test.tsx.snap | 1 + .../AdvancedFilter/FilterContentContainer.tsx | 2 - .../AdvancedFilter/FilterContentForm.tsx | 2 - .../src/components/modals/roleMismatch.tsx | 2 +- source/frontend/src/contexts/modalContext.tsx | 7 +- .../Organization/CreateOrganizationForm.tsx | 2 + .../create/Person/CreatePersonForm.tsx | 2 + .../Organization/UpdateOrganizationForm.tsx | 1 + .../contact/edit/Person/UpdatePersonForm.tsx | 1 + .../documentDetail/DocumentDetailModal.tsx | 2 +- .../documentUpload/DocumentUploadModal.tsx | 2 +- .../documents/list/DocumentListView.tsx | 2 +- .../LeasePages/deposits/DepositsContainer.tsx | 2 + .../payment/TermPaymentsContainer.tsx | 2 + .../tenant/PrimaryContactWarningModal.tsx | 1 + .../propertyPicker/LeasePropertySelector.tsx | 3 +- .../acquisition/AcquisitionView.test.tsx | 4 +- .../acquisition/AcquisitionView.tsx | 2 +- .../modals/GenerateLetterRecipientsModal.tsx | 2 +- ...enerateLetterRecipientsModal.test.tsx.snap | 114 ++++++++++++++- .../common/modals/AcquisitionFormModal.tsx | 7 +- .../agreement/update/AgreementSubForm.tsx | 1 + .../UpdateCompensationRequisitionForm.tsx | 2 + .../financials/FinancialActivitiesSubForm.tsx | 1 + .../update/UpdateAcquisitionContainer.tsx | 1 + .../mapSideBar/lease/LeaseContainer.tsx | 2 +- .../project/ProjectContainerView.tsx | 1 + .../project/add/ProductsArrayForm.tsx | 2 + .../property/MotiInventoryContainer.tsx | 1 + .../detail/PropertyContactList.tsx | 1 + .../tabs/takes/update/TakeSubForm.tsx | 1 + .../mapSideBar/research/ResearchContainer.tsx | 1 + .../update/properties/UpdateProperties.tsx | 3 + .../__snapshots__/NoteContainer.test.tsx.snap | 96 ++++++++++++- .../features/notes/add/AddNotesContainer.tsx | 1 + .../AddNotesContainer.test.tsx.snap | 96 ++++++++++++- .../AddNotesFormModal.test.tsx.snap | 96 ++++++++++++- .../NoteDetailsFormModal.test.tsx.snap | 96 ++++++++++++- .../src/features/notes/list/NoteListView.tsx | 1 + .../notes/update/UpdateNoteContainer.tsx | 1 + .../UpdateNoteContainer.test.tsx.snap | 96 ++++++++++++- .../UpdateNoteFormModal.test.tsx.snap | 96 ++++++++++++- .../reports/ProjectExportContainer.tsx | 2 + .../frontend/src/hooks/useApiUserOverride.ts | 3 +- source/frontend/src/hooks/useModalContext.ts | 6 +- .../frontend/src/hooks/usePimsIdleTimer.tsx | 2 +- .../frontend/src/utils/TestCommonWrapper.tsx | 1 - 59 files changed, 1011 insertions(+), 236 deletions(-) diff --git a/source/frontend/src/assets/scss/App.scss b/source/frontend/src/assets/scss/App.scss index 38427edf2e..36cd16ad38 100644 --- a/source/frontend/src/assets/scss/App.scss +++ b/source/frontend/src/assets/scss/App.scss @@ -1,114 +1,5 @@ @import './styles.scss'; -.modal-dialog { - div.modal-header { - height: 4.8rem; - padding: 0 1.6rem; - display: flex; - flex-direction: row; - align-items: center; - color: $primary-background-color; - background-color: $primary-color; - - .modal-title { - font-family: BcSans-Bold; - font-size: 2.2rem; - height: 2.75rem; - height: auto; - } - - .header-icon { - margin-right: 8px; - display: inline-block; - } - - .modal-close-btn { - cursor: pointer; - } - } - - div.modal-body { - padding: 2.4rem 1.8rem; - font-size: 1.8rem; - } - - div.modal-footer { - border-top: none; - - hr { - width: 100%; - } - - .button-wrap { - display: inline-flex; - margin-top: 2.4rem; - margin-bottom: 2.4rem; - } - - button { - margin-right: 2.4rem; - min-width: 9.5rem; - height: 3.9rem; - } - } - - .close { - color: black; - } - - &.modal-xl { - max-width: 100rem; - } - - &.modal-l { - max-width: 75rem; - } - - &.modal-m { - max-width: 50rem; - } - - &.modal-s { - max-width: 40rem; - } - - &.info { - .modal-header { - color: $dark-blue; - background-color: $filter-box-color; - } - - .modal-close-btn { - color: $text-color; - cursor: pointer; - } - } - - &.error { - .modal-header { - color: $font-danger-color; - background-color: $danger-background-color; - } - - .modal-close-btn { - color: $text-color; - cursor: pointer; - } - } - - &.warning { - .modal-header { - color: $font-warning-color; - background-color: $summary-color; - } - - .modal-close-btn { - color: $text-color; - cursor: pointer; - } - } -} - .Toastify__toast .Toastify__toast-body { word-break: break-word; } diff --git a/source/frontend/src/assets/scss/_variables.module.scss b/source/frontend/src/assets/scss/_variables.module.scss index 3db8bdbbf9..c4fa65104e 100644 --- a/source/frontend/src/assets/scss/_variables.module.scss +++ b/source/frontend/src/assets/scss/_variables.module.scss @@ -6,40 +6,6 @@ footerHeight: $footer-height; headerHeight: $header-height; - primaryColor: $primary-color; - primaryLightColor: $primary-light-color; - secondaryVariantColor: $secondary-variant-color; - selectedColor: $selected-color; - lightVariantColor: $light-variant-color; - darkVariantColor: $dark-variant-color; - textColor: $text-color; - formTextColor: $form-text-color; - formControlTextColor: $form-control-text-color; - formBackgroundColor: $form-background-color; - primaryBackgroundColor: $primary-background-color; - dropdownBackgroundColor: $dropdown-background-color; - accentColor: $accent-color; - lightAccentColor: $light-accent-color; - sresIconColor: $sres-icon-color; - dangerColor: $danger-color; - lightDangerColor: $light-danger-color; - iconLightColor: $icon-light-color; - activeColor: $active-color; - completedColor: $completed-color; - filterBackgroundColor: $filter-background-color; - slideOutBlue: $slide-out-blue; - disabledColor: $disabled-color; - disabledFieldBackgroundColor: $disabled-field-background-color; - filterBoxColor: $filter-box-color; - draftColor: $draft-color; - linkColor: $link-color; - linkHoverColor: $link-hover-color; - subtleColor: $subtle-color; - discardedColor: $discarded-color; - dangerBackgroundColor: $danger-background-color; - summaryColor: $summary-color; - summaryBorderColor: $summary-border-color; - primaryBorderColor: $primary-border-color; // table ui tableHoverColor: $table-hover-color; tableHeaderBgColor: $table-header-bg-color; @@ -55,4 +21,43 @@ // buttons buttonOutlineColor: $icon-light-color; buttonInfoColor: $button-info-color; + + // Colors + accentColor: $accent-color; + activeColor: $active-color; + completedColor: $completed-color; + dangerBackgroundColor: $danger-background-color; + dangerColor: $danger-color; + darkBlue: $dark-blue; + darkVariantColor: $dark-variant-color; + disabledColor: $disabled-color; + disabledFieldBackgroundColor: $disabled-field-background-color; + discardedColor: $discarded-color; + draftColor: $draft-color; + dropdownBackgroundColor: $dropdown-background-color; + filterBackgroundColor: $filter-background-color; + filterBoxColor: $filter-box-color; + fontDangerColor: $font-danger-color; + fontWarningColor: $font-warning-color; + formBackgroundColor: $form-background-color; + formControlTextColor: $form-control-text-color; + formTextColor: $form-text-color; + iconLightColor: $icon-light-color; + lightAccentColor: $light-accent-color; + lightDangerColor: $light-danger-color; + lightVariantColor: $light-variant-color; + linkColor: $link-color; + linkHoverColor: $link-hover-color; + primaryBackgroundColor: $primary-background-color; + primaryBorderColor: $primary-border-color; + primaryColor: $primary-color; + primaryLightColor: $primary-light-color; + secondaryVariantColor: $secondary-variant-color; + selectedColor: $selected-color; + slideOutBlue: $slide-out-blue; + sresIconColor: $sres-icon-color; + subtleColor: $subtle-color; + summaryBorderColor: $summary-border-color; + summaryColor: $summary-color; + textColor: $text-color; } diff --git a/source/frontend/src/components/common/CancelConfirmationModal.tsx b/source/frontend/src/components/common/CancelConfirmationModal.tsx index 8e9fad5dff..34068cd965 100644 --- a/source/frontend/src/components/common/CancelConfirmationModal.tsx +++ b/source/frontend/src/components/common/CancelConfirmationModal.tsx @@ -2,8 +2,11 @@ import React from 'react'; import GenericModal, { ModalProps } from '@/components/common/GenericModal'; -export const CancelConfirmationModal: React.FC> = props => { +export const CancelConfirmationModal: React.FC< + React.PropsWithChildren> +> = props => { const { + variant = 'info', title = 'Unsaved Changes', message = 'You have made changes on this form. Do you wish to leave without saving?', okButtonText = 'Confirm', @@ -13,6 +16,7 @@ export const CancelConfirmationModal: React.FC { const history = useHistory(); return ( {showTabErrorModal && ( { - const { asFragment } = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); it('renders title based off of prop...', () => { - const { getByText } = render(); + const { getByText } = render(); expect(getByText('Test Title')).toBeInTheDocument(); }); it('renders message based off of prop', () => { - const { getByText } = render(); + const { getByText } = render(); expect(getByText('Test Message')).toBeInTheDocument(); }); it('renders button text based off of prop', () => { const { getByText } = render( - , + , ); expect(getByText('Ok Text')).toBeInTheDocument(); expect(getByText('Cancel Text')).toBeInTheDocument(); @@ -27,7 +27,9 @@ it('renders button text based off of prop', () => { it('calls custom ok on click', async () => { const mockOk = jest.fn(); - const { getByText } = render(); + const { getByText } = render( + , + ); const okButton = getByText('Ok Button'); await waitFor(() => { fireEvent.click(okButton); @@ -38,7 +40,7 @@ it('calls custom ok on click', async () => { it('calls custom cancel funciton on click', async () => { const mockCancel = jest.fn(); const { getByText } = render( - , + , ); const cancelButton = getByText('Cancel Button'); await waitFor(() => { diff --git a/source/frontend/src/components/common/GenericModal.tsx b/source/frontend/src/components/common/GenericModal.tsx index 01029dffef..108f19b997 100644 --- a/source/frontend/src/components/common/GenericModal.tsx +++ b/source/frontend/src/components/common/GenericModal.tsx @@ -77,6 +77,8 @@ export interface ModalContent { closeButton?: boolean; /** provide the size of the modal, default width is 50.0rem */ modalSize?: ModalSize; + variant: 'info' | 'warning' | 'error'; + //variant?: string; className?: string; /** display this modal as a popup instead of as a modal, allowing the user to click on underlying elements */ asPopup?: boolean; @@ -106,6 +108,7 @@ export const GenericModal = (props: Omit & ModalProps) = closeButton, hideFooter, modalSize, + variant, className, headerIcon, ...rest @@ -144,7 +147,7 @@ export const GenericModal = (props: Omit & ModalProps) = return <>{headerIcon}; } - switch (className) { + switch (variant) { case 'info': case 'warning': { return ; @@ -158,15 +161,36 @@ export const GenericModal = (props: Omit & ModalProps) = } }; + function getVariantClass() { + switch (variant) { + case 'info': + return 'info-variant'; + case 'warning': { + return 'warning-variant'; + } + case 'error': { + return 'error-variant'; + } + default: { + return 'info-variant'; + } + } + } + + function getModalClass() { + return (className || '') + ' ' + getVariantClass(); + } + const headerIconValue = getHeaderIcon(); return ( @@ -238,6 +262,113 @@ const StyledModal = styled(Modal)` font-family: 'Helvetica Narrow'; } } + + .modal-header { + height: 4.8rem; + padding: 0 1.6rem; + display: flex; + flex-direction: row; + align-items: center; + color: ${props => props.theme.css.primaryBackgroundColor}; + background-color: ${props => props.theme.css.primaryColor}; + + .modal-title { + font-family: BcSans-Bold; + font-size: 2.2rem; + height: 2.75rem; + height: auto; + } + + .header-icon { + margin-right: 8px; + display: inline-block; + } + + .modal-close-btn { + cursor: pointer; + } + } + + .modal-body { + padding: 2.4rem 1.8rem; + font-size: 1.8rem; + } + + .modal-footer { + border-top: none; + + hr { + width: 100%; + } + + .button-wrap { + display: inline-flex; + margin-top: 2.4rem; + margin-bottom: 2.4rem; + + button { + margin-right: 2.4rem; + min-width: 9.5rem; + height: 3.9rem; + } + } + } + + .close { + color: black; + } + + &.modal-xl { + max-width: 100rem; + } + + &.modal-l { + max-width: 75rem; + } + + &.modal-m { + max-width: 50rem; + } + + &.modal-s { + max-width: 40rem; + } + + &.info-variant { + .modal-header { + color: ${props => props.theme.css.darkBlue}; + background-color: ${props => props.theme.css.filterBoxColor}; + } + + .modal-close-btn { + color: ${props => props.theme.css.textColor}; + cursor: pointer; + } + } + + &.error-variant { + .modal-header { + color: ${props => props.theme.css.fontDangerColor}; + background-color: ${props => props.theme.css.dangerBackgroundColor}; + } + + .modal-close-btn { + color: ${props => props.theme.css.textColor}; + cursor: pointer; + } + } + + &.warning-variant { + .modal-header { + color: ${props => props.theme.css.fontWarningColor}; + background-color: ${props => props.theme.css.summaryColor}; + } + + .modal-close-btn { + color: ${props => props.theme.css.textColor}; + cursor: pointer; + } + } `; const PopupContainer = styled.div` diff --git a/source/frontend/src/components/common/ModalContainer.test.tsx b/source/frontend/src/components/common/ModalContainer.test.tsx index f104e9491c..5fd5dd193c 100644 --- a/source/frontend/src/components/common/ModalContainer.test.tsx +++ b/source/frontend/src/components/common/ModalContainer.test.tsx @@ -25,7 +25,7 @@ describe('ModalContainer component', () => { // render component under test const component = render( <> - + , { ...renderOptions, @@ -44,7 +44,7 @@ describe('ModalContainer component', () => { }); it('displays a modal based on props', async () => { - setup({ modalProps: { title: 'test', message: 'test 2' }, isVisible: true }); + setup({ modalProps: { variant: 'info', title: 'test', message: 'test 2' }, isVisible: true }); expect(await screen.findByText('test')).toBeVisible(); expect(await screen.findByText('test 2')).toBeVisible(); @@ -52,7 +52,7 @@ describe('ModalContainer component', () => { it('shows/hides modal', async () => { setup({ - modalProps: { title: 'test', message: 'test 2', okButtonText: 'ok' }, + modalProps: { variant: 'info', title: 'test', message: 'test 2', okButtonText: 'ok' }, isVisible: true, }); diff --git a/source/frontend/src/components/common/form/NotesModal.tsx b/source/frontend/src/components/common/form/NotesModal.tsx index 169c88fea7..a276eab42d 100644 --- a/source/frontend/src/components/common/form/NotesModal.tsx +++ b/source/frontend/src/components/common/form/NotesModal.tsx @@ -42,7 +42,7 @@ export const NotesModal: React.FunctionComponent { planNumberInventoryData = await loadPropertiesTask; } catch (err) { setModalContent({ - className: 'info', + variant: 'error', title: 'Unable to connect to PIMS Inventory', message: 'PIMS is unable to connect to connect to the PIMS Inventory map service. You may need to log out and log into the application in order to restore this functionality. If this error persists, contact a site administrator.', @@ -235,6 +235,7 @@ export const useMapSearch = () => { pidPinInventoryData = await loadPropertiesTask; } catch (err) { setModalContent({ + variant: 'error', title: 'Unable to connect to PIMS Inventory', message: 'PIMS is unable to connect to connect to the PIMS Inventory map service. You may need to log out and log into the application in order to restore this functionality. If this error persists, contact a site administrator.', diff --git a/source/frontend/src/components/contact/ContactManagerModal.tsx b/source/frontend/src/components/contact/ContactManagerModal.tsx index f34948d8f1..4538a2aa41 100644 --- a/source/frontend/src/components/contact/ContactManagerModal.tsx +++ b/source/frontend/src/components/contact/ContactManagerModal.tsx @@ -23,7 +23,7 @@ export const ContactManagerModal: React.FunctionComponent< > = props => { return ( { const dispatch = useDispatch(); - const handleClose = () => setShow(false); + const handleClose = () => { + setShow(false); + }; const handleClear = () => { errors.forEach(error => dispatch(logClear(error.name))); setShow(false); }; - return ( - - - Errors - + const errorUrl = (error: IGenericNetworkAction): string => { + return error?.error?.response?.config?.url || ''; + }; - - {errors.map((error: IGenericNetworkAction, index: number) => ( - - {process.env.NODE_ENV === 'development' ? ( - - - {error?.error?.response?.config?.url?.substr(0, 20)} - - : {error?.error?.response?.statusText} data:{' '} - {JSON.stringify(error?.error?.response?.data)} - - ) : ( - - - {error?.error?.response?.config?.url?.substr(0, 20)} - - : ({error?.error?.response?.statusText ?? 'unknown'}){' '} - {(error?.error?.response?.data as unknown & { error: string })?.error ?? ''} - - )} - - ))} - + const errorShortUrl = (error: IGenericNetworkAction): string => { + var url = errorUrl(error); + if (url.length > 20) { + return error?.error?.response?.config?.url?.substr(0, 20) + '...'; + } else { + return url; + } + }; - - - - + const errorStatus = (error: IGenericNetworkAction): string => { + return error?.error?.response?.statusText || 'unknown'; + }; + + return ( + + {errors.map((error: IGenericNetworkAction, index: number) => ( + + {process.env.NODE_ENV === 'development' ? ( +
+ + {errorStatus(error)} + + + {errorUrl(error)} + + + + {JSON.stringify(error?.error?.response?.data)} + + +
+ ) : ( +
+ + {errorStatus(error)} + + + {errorUrl(error)} + + + + {(error?.error?.response?.data as unknown & { error: string })?.error ?? ''} + + +
+ )} +
+ ))} + + } + /> ); }; export default ErrorModal; + +const ErrorWrapper = styled(StyledGreySection)` + max-height: 70rem; + overflow-y: scroll; + width: 100%; + padding: 0; +`; + +const ErrorEntry = styled.div``; + +const ErrorDescription = styled.div` + word-break: break-all; +`; diff --git a/source/frontend/src/components/layout/Header/__snapshots__/Header.test.tsx.snap b/source/frontend/src/components/layout/Header/__snapshots__/Header.test.tsx.snap index c8e836e0a6..2a1a5855eb 100644 --- a/source/frontend/src/components/layout/Header/__snapshots__/Header.test.tsx.snap +++ b/source/frontend/src/components/layout/Header/__snapshots__/Header.test.tsx.snap @@ -181,6 +181,7 @@ exports[`App Header renders correctly 1`] = `