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/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/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 0707fea78e..0372e442b5 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(); 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/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", 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;