diff --git a/source/backend/api/Areas/Takes/Controllers/TakeController.cs b/source/backend/api/Areas/Takes/Controllers/TakeController.cs index 6061efe24c..162dffc5c4 100644 --- a/source/backend/api/Areas/Takes/Controllers/TakeController.cs +++ b/source/backend/api/Areas/Takes/Controllers/TakeController.cs @@ -1,15 +1,18 @@ using System; using System.Collections.Generic; +using System.Linq; using MapsterMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Pims.Api.Helpers.Exceptions; using Pims.Api.Models.Concepts.Take; using Pims.Api.Policies; using Pims.Api.Services; using Pims.Core.Extensions; using Pims.Core.Json; using Pims.Dal.Entities; +using Pims.Dal.Exceptions; using Pims.Dal.Security; using Swashbuckle.AspNetCore.Annotations; @@ -104,28 +107,98 @@ public IActionResult GetTakesByPropertyId([FromRoute] long fileId, [FromRoute] l } /// - /// Update the list of takes associated to a property within an acquisition file. + /// Add the passed take to the acquisition property with the given id. /// /// - [HttpPut("acquisition/property/{acquisitionFilePropertyId:long}")] + [HttpPost("acquisition/property/{acquisitionFilePropertyId:long}/takes")] [HasPermission(Permissions.AcquisitionFileEdit, Permissions.PropertyEdit)] [Produces("application/json")] - [ProducesResponseType(typeof(IEnumerable), 200)] + [ProducesResponseType(typeof(TakeModel), 201)] + [SwaggerOperation(Tags = new[] { "take" })] + [TypeFilter(typeof(NullJsonResultFilter))] + public IActionResult AddAcquisitionPropertyTake(long acquisitionFilePropertyId, [FromBody] TakeModel take) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(TakeController), + nameof(AddAcquisitionPropertyTake), + User.GetUsername(), + DateTime.Now); + + if (acquisitionFilePropertyId != take.PropertyAcquisitionFileId) + { + throw new BadRequestException("Invalid acquisition file property id."); + } + + _logger.LogInformation("Dispatching to service: {Service}", _takeService.GetType()); + + var addedTake = _takeService.AddAcquisitionPropertyTake(acquisitionFilePropertyId, _mapper.Map(take)); + return new JsonResult(_mapper.Map(addedTake)); + } + + /// + /// Update a take with the given take and acquisition file property id with the passed take. + /// + /// + [HttpPut("acquisition/property/{acquisitionFilePropertyId:long}/takes/{takeId:long}")] + [HasPermission(Permissions.AcquisitionFileEdit, Permissions.PropertyEdit)] + [Produces("application/json")] + [ProducesResponseType(typeof(TakeModel), 200)] [SwaggerOperation(Tags = new[] { "take" })] [TypeFilter(typeof(NullJsonResultFilter))] - public IActionResult UpdateAcquisitionPropertyTakes(long acquisitionFilePropertyId, [FromBody] IEnumerable takes) + public IActionResult UpdateAcquisitionPropertyTake(long acquisitionFilePropertyId, long takeId, [FromBody] TakeModel take) { _logger.LogInformation( "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", nameof(TakeController), - nameof(UpdateAcquisitionPropertyTakes), + nameof(UpdateAcquisitionPropertyTake), User.GetUsername(), DateTime.Now); + if (acquisitionFilePropertyId != take.PropertyAcquisitionFileId) + { + throw new BadRequestException("Invalid acquisition file property id."); + } + else if (takeId != take.Id) + { + throw new BadRequestException("Invalid take id."); + } + _logger.LogInformation("Dispatching to service: {Service}", _takeService.GetType()); - var updatedTakes = _takeService.UpdateAcquisitionPropertyTakes(acquisitionFilePropertyId, _mapper.Map>(takes)); - return new JsonResult(_mapper.Map>(updatedTakes)); + var updatedTake = _takeService.UpdateAcquisitionPropertyTake(acquisitionFilePropertyId, _mapper.Map(take)); + return new JsonResult(_mapper.Map(updatedTake)); + } + + /// + /// Delete a take with the given take id and acquisition file property id. + /// + [HttpDelete("acquisition/property/{acquisitionFilePropertyId:long}/takes/{takeId:long}")] + [HasPermission(Permissions.AcquisitionFileEdit, Permissions.PropertyEdit)] + [Produces("application/json")] + [ProducesResponseType(typeof(void), 200)] + [SwaggerOperation(Tags = new[] { "take" })] + [TypeFilter(typeof(NullJsonResultFilter))] + public void DeleteAcquisitionPropertyTake(long acquisitionFilePropertyId, long takeId, [FromQuery] string[] userOverrideCodes) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(TakeController), + nameof(DeleteAcquisitionPropertyTake), + User.GetUsername(), + DateTime.Now); + + _logger.LogInformation("Dispatching to service: {Service}", _takeService.GetType()); + var existingTake = _takeService.GetById(takeId); + if (existingTake.PropertyAcquisitionFileId != acquisitionFilePropertyId) + { + throw new BadRequestException("Invalid acquisition file property id."); + } + var deleted = _takeService.DeleteAcquisitionPropertyTake(takeId, userOverrideCodes.Select(oc => UserOverrideCode.Parse(oc))); + if (!deleted) + { + throw new InvalidOperationException($"Failed to delete take {takeId}."); + } } /// @@ -152,6 +225,34 @@ public IActionResult GetTakesCountByPropertyId([FromRoute] long propertyId) return new JsonResult(count); } + /// + /// GGet a take by id. + /// + /// + [HttpGet("acquisition/property/{acquisitionFilePropertyId:long}/takes/{takeId:long}")] + [HasPermission(Permissions.AcquisitionFileView, Permissions.PropertyView)] + [Produces("application/json")] + [ProducesResponseType(typeof(int), 200)] + [SwaggerOperation(Tags = new[] { "take" })] + public IActionResult GetTakeByPropertyFileId(long acquisitionFilePropertyId, long takeId) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(TakeController), + nameof(GetTakesCountByPropertyId), + User.GetUsername(), + DateTime.Now); + + _logger.LogInformation("Dispatching to service: {Service}", _takeService.GetType()); + + var take = _takeService.GetById(takeId); + if(take.PropertyAcquisitionFileId != acquisitionFilePropertyId) + { + throw new BadRequestException("Invalid acquisition file property id."); + } + return new JsonResult(_mapper.Map(take)); + } + #endregion } } diff --git a/source/backend/api/Services/ITakeService.cs b/source/backend/api/Services/ITakeService.cs index 5758235c33..6548486492 100644 --- a/source/backend/api/Services/ITakeService.cs +++ b/source/backend/api/Services/ITakeService.cs @@ -1,16 +1,24 @@ using System.Collections.Generic; using Pims.Dal.Entities; +using Pims.Dal.Exceptions; namespace Pims.Api.Services { public interface ITakeService { + + PimsTake GetById(long takeId); + + PimsTake AddAcquisitionPropertyTake(long acquisitionFilePropertyId, PimsTake take); + + PimsTake UpdateAcquisitionPropertyTake(long acquisitionFilePropertyId, PimsTake take); + + bool DeleteAcquisitionPropertyTake(long takeId, IEnumerable userOverrides); + IEnumerable GetByFileId(long fileId); IEnumerable GetByPropertyId(long fileId, long acquisitionFilePropertyId); int GetCountByPropertyId(long propertyId); - - IEnumerable UpdateAcquisitionPropertyTakes(long acquisitionFilePropertyId, IEnumerable takes); } } diff --git a/source/backend/api/Services/TakeService.cs b/source/backend/api/Services/TakeService.cs index fb5698f7b1..cad1988544 100644 --- a/source/backend/api/Services/TakeService.cs +++ b/source/backend/api/Services/TakeService.cs @@ -7,6 +7,7 @@ using Pims.Api.Models.CodeTypes; using Pims.Core.Exceptions; using Pims.Dal.Entities; +using Pims.Dal.Exceptions; using Pims.Dal.Helpers.Extensions; using Pims.Dal.Repositories; using Pims.Dal.Security; @@ -43,6 +44,13 @@ public TakeService( _propertyRepository = propertyRepository; } + public PimsTake GetById(long takeId) + { + _logger.LogInformation("Getting take with takeId {takeId}", takeId); + _user.ThrowIfNotAuthorized(Permissions.PropertyView, Permissions.AcquisitionFileView); + return _takeRepository.GetById(takeId); + } + public IEnumerable GetByFileId(long fileId) { _logger.LogInformation("Getting takes with fileId {fileId}", fileId); @@ -64,65 +72,176 @@ public int GetCountByPropertyId(long propertyId) return _takeRepository.GetCountByPropertyId(propertyId); } - public IEnumerable UpdateAcquisitionPropertyTakes(long acquisitionFilePropertyId, IEnumerable takes) + public PimsTake AddAcquisitionPropertyTake(long acquisitionFilePropertyId, PimsTake take) { - _logger.LogInformation("updating takes with propertyFileId {propertyFileId}", acquisitionFilePropertyId); + _logger.LogInformation("adding take with propertyFileId {propertyFileId}", acquisitionFilePropertyId); _user.ThrowIfNotAuthorized(Permissions.PropertyView, Permissions.AcquisitionFileView); - var currentAcquistionFile = _acqFileRepository.GetByAcquisitionFilePropertyId(acquisitionFilePropertyId); + ValidateTakeRules(acquisitionFilePropertyId, take); + + // Add take + var addedTake = _takeRepository.AddTake(take); + + RecalculateOwnership(acquisitionFilePropertyId, take); + + _takeRepository.CommitTransaction(); + return addedTake; + } + + public PimsTake UpdateAcquisitionPropertyTake(long acquisitionFilePropertyId, PimsTake take) + { + _logger.LogInformation("updating take with propertyFileId {propertyFileId}", acquisitionFilePropertyId); + _user.ThrowIfNotAuthorized(Permissions.PropertyView, Permissions.AcquisitionFileView); + + ValidateTakeRules(acquisitionFilePropertyId, take); + + // Update take + var updatedTake = _takeRepository.UpdateTake(take); + + RecalculateOwnership(acquisitionFilePropertyId, take); + + _takeRepository.CommitTransaction(); + return updatedTake; + } + + public bool DeleteAcquisitionPropertyTake(long takeId, IEnumerable userOverrides) + { + _logger.LogInformation("deleting take with {takeId}", takeId); + _user.ThrowIfNotAuthorized(Permissions.PropertyView, Permissions.AcquisitionFileView); + + var takeToDelete = _takeRepository.GetById(takeId); + var propertyWithAssociations = _propertyRepository.GetAllAssociationsById(takeToDelete.PropertyAcquisitionFile.PropertyId); + var currentAcquisitionFile = _acqFileRepository.GetByAcquisitionFilePropertyId(takeToDelete.PropertyAcquisitionFileId); + var allTakesForProperty = _takeRepository.GetAllByPropertyId(takeToDelete.PropertyAcquisitionFile.PropertyId); + if ((!_statusSolver.CanEditTakes(Enum.Parse(currentAcquisitionFile.AcquisitionFileStatusTypeCode)) || takeToDelete.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString()) + && !_user.HasPermission(Permissions.SystemAdmin)) + { + throw new BusinessRuleViolationException("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion."); + } + else if(propertyWithAssociations?.PimsDispositionFileProperties?.Any(d => d.DispositionFile.DispositionFileStatusTypeCode == DispositionFileStatusTypes.COMPLETE.ToString()) == true) + { + throw new BusinessRuleViolationException("You cannot delete a take that has a completed disposition attached to the same property."); + } + else if (propertyWithAssociations?.IsRetired == true) + { + throw new BusinessRuleViolationException("You cannot delete a take from a retired property."); + } + + // user overrides + if (takeToDelete.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString() && _user.HasPermission(Permissions.SystemAdmin)) + { + if (propertyWithAssociations?.PimsDispositionFileProperties?.Any(d => d.DispositionFile.DispositionFileStatusTypeCode == DispositionFileStatusTypes.ACTIVE.ToString()) == true && !userOverrides.Contains(UserOverrideCode.DeleteTakeActiveDisposition)) + { + throw new UserOverrideException(UserOverrideCode.DeleteTakeActiveDisposition, "You are deleting a take. Property ownership state will be recalculated based upon any remaining completed takes. It should be noted that one or more related dispositions are in progress that should also be reviewed. \n\nDo you want to acknowledge and proceed?"); + } + else if (allTakesForProperty.Count(t => !IsTakeExpired(t) && t.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString()) == 1 && allTakesForProperty.FirstOrDefault().TakeId == takeId && !userOverrides.Contains(UserOverrideCode.DeleteLastTake)) + { + throw new UserOverrideException(UserOverrideCode.DeleteLastTake, "You are deleting the last non-expired completed take on this property. This property will become a property of interest.\n\nDo you want to acknowledge and proceed?"); + } + else if (!userOverrides.Contains(UserOverrideCode.DeleteCompletedTake) && !userOverrides.Contains(UserOverrideCode.DeleteLastTake) && !userOverrides.Contains(UserOverrideCode.DeleteTakeActiveDisposition)) + { + throw new UserOverrideException(UserOverrideCode.DeleteCompletedTake, "You are deleting a completed take. Property ownership state will be recalculated based upon any remaining completed takes.\n\nDo you want to acknowledge and proceed?"); + } + } + + var wasTakeDeleted = _takeRepository.TryDeleteTake(takeId); + + if (wasTakeDeleted) + { + // Evaluate if the property needs to be updated + var currentProperty = _acqFileRepository.GetProperty(takeToDelete.PropertyAcquisitionFileId); + var currentTakes = _takeRepository.GetAllByPropertyId(currentProperty.PropertyId); + + var completedTakes = currentTakes + .Where(x => x.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString() && x.TakeId != takeId).ToList(); + + if (completedTakes.Count > 0 || takeToDelete.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString()) + { + if (_takeInteractionSolver.ResultsInOwnedProperty(completedTakes)) + { + _propertyRepository.TransferFileProperty(currentProperty, true); + } + else + { + _propertyRepository.TransferFileProperty(currentProperty, false); + } + } + } + + _takeRepository.CommitTransaction(); + return wasTakeDeleted; + } + + private static bool IsTakeExpired(PimsTake take) + { + return (take.IsActiveLease && take.ActiveLeaseEndDt > DateOnly.FromDateTime(DateTime.Now)) + || (take.IsNewLandAct && take.LandActEndDt > DateOnly.FromDateTime(DateTime.Now)) + || (take.IsNewLicenseToConstruct && take.LtcEndDt > DateOnly.FromDateTime(DateTime.Now)) + || (take.IsNewInterestInSrw && take.SrwEndDt > DateOnly.FromDateTime(DateTime.Now)); + } + + private void ValidateTakeRules(long acquisitionFilePropertyId, PimsTake take) + { var currentFilePropertyTakes = _takeRepository.GetAllByPropertyAcquisitionFileId(acquisitionFilePropertyId); + var currentAcquistionFile = _acqFileRepository.GetByAcquisitionFilePropertyId(acquisitionFilePropertyId); var currentAcquisitionStatus = Enum.Parse(currentAcquistionFile.AcquisitionFileStatusTypeCode); if (!_statusSolver.CanEditTakes(currentAcquisitionStatus) && !_user.HasPermission(Permissions.SystemAdmin)) { throw new BusinessRuleViolationException("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion."); } - else if (takes.Any(t => t.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString() && t.CompletionDt == null)) + else if (take.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString() && take.CompletionDt == null) { throw new BusinessRuleViolationException("A completed take must have a completion date."); } - else if (takes.Any(t => t.IsNewLandAct && t.LandActEndDt != null && (t.LandActTypeCode == LandActTypes.CROWN_GRANT.ToString() || t.LandActTypeCode == LandActTypes.TRANSFER_OF_ADMIN_AND_CONTROL.ToString()))) + else if (take.IsNewLandAct && take.LandActEndDt != null && (take.LandActTypeCode == LandActTypes.CROWN_GRANT.ToString() || take.LandActTypeCode == LandActTypes.TRANSFER_OF_ADMIN_AND_CONTROL.ToString())) { throw new BusinessRuleViolationException("'Crown Grant' and 'Transfer' Land Acts cannot have an end date."); } else { - // Complete Takes can only be deleted or set to InProgress by Admins when File is Active/Draft + // Complete Takes can only be set to InProgress by Admins when File is Active/Draft var currentCompleteTakes = currentFilePropertyTakes .Where(x => x.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString()).ToList(); if (currentCompleteTakes.Count > 0) { - foreach (var completeTake in currentCompleteTakes) + var updatedTake = currentCompleteTakes.FirstOrDefault(x => x.TakeId == take.TakeId); + if (!_user.HasPermission(Permissions.SystemAdmin) && (updatedTake is not null && updatedTake.TakeStatusTypeCode != take.TakeStatusTypeCode)) { - // Validate that the current completed take can only by updated by a sysadmin - var updatedTake = takes.FirstOrDefault(x => x.TakeId == completeTake.TakeId); - if (!_user.HasPermission(Permissions.SystemAdmin) && (updatedTake is null || (updatedTake is not null && updatedTake.TakeStatusTypeCode != completeTake.TakeStatusTypeCode))) - { - throw new BusinessRuleViolationException("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion."); - } + throw new BusinessRuleViolationException("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion."); } } } + } - // Update takes - _takeRepository.UpdateAcquisitionPropertyTakes(acquisitionFilePropertyId, takes); - + private void RecalculateOwnership(long acquisitionFilePropertyId, PimsTake take) + { // Evaluate if the property needs to be updated var currentProperty = _acqFileRepository.GetProperty(acquisitionFilePropertyId); var currentTakes = _takeRepository.GetAllByPropertyId(currentProperty.PropertyId); - var completedTakes = currentTakes.Union(takes) + var allTakes = currentTakes; + if (take != null) + { + allTakes = allTakes.Append(take); + } + + var completedTakes = allTakes .Where(x => x.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString()).ToList(); - if (_takeInteractionSolver.ResultsInOwnedProperty(completedTakes)) + if (completedTakes.Count > 0) { - _propertyRepository.TransferFileProperty(currentProperty, true); + if (_takeInteractionSolver.ResultsInOwnedProperty(completedTakes)) + { + _propertyRepository.TransferFileProperty(currentProperty, true); + } + else + { + _propertyRepository.TransferFileProperty(currentProperty, false); + } } - - _takeRepository.CommitTransaction(); - return _takeRepository.GetAllByPropertyAcquisitionFileId(acquisitionFilePropertyId); } } } diff --git a/source/backend/apimodels/CodeTypes/TakeTypes.cs b/source/backend/apimodels/CodeTypes/TakeTypes.cs new file mode 100644 index 0000000000..843e528b32 --- /dev/null +++ b/source/backend/apimodels/CodeTypes/TakeTypes.cs @@ -0,0 +1,15 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Pims.Api.Models.CodeTypes +{ + [JsonConverter(typeof(JsonStringEnumMemberConverter))] + public enum TakeTypes + { + [EnumMember(Value = "TOTAL")] + TOTAL, + + [EnumMember(Value = "PARTIAL")] + PARTIAL, + } +} diff --git a/source/backend/dal/Exceptions/OverrideExceptions.cs b/source/backend/dal/Exceptions/OverrideExceptions.cs index cfd4fa58b7..df35cd7bec 100644 --- a/source/backend/dal/Exceptions/OverrideExceptions.cs +++ b/source/backend/dal/Exceptions/OverrideExceptions.cs @@ -50,6 +50,21 @@ public static UserOverrideCode DisposeOfProperties get { return new UserOverrideCode("DISPOSE_OF_PROPERTIES"); } } + public static UserOverrideCode DeleteCompletedTake + { + get { return new UserOverrideCode("DELETE_COMPLETED_TAKE"); } + } + + public static UserOverrideCode DeleteLastTake + { + get { return new UserOverrideCode("DELETE_LAST_TAKE"); } + } + + public static UserOverrideCode DeleteTakeActiveDisposition + { + get { return new UserOverrideCode("DELETE_TAKE_ACTIVE_DISPOSITION"); } + } + public string Code { get; private set; } private static List UserOverrideCodes => new List() @@ -63,6 +78,9 @@ public static UserOverrideCode DisposeOfProperties UserOverrideCode.DisposingPropertyNotInventoried, UserOverrideCode.DispositionFileFinalStatus, UserOverrideCode.DisposeOfProperties, + UserOverrideCode.DeleteCompletedTake, + UserOverrideCode.DeleteLastTake, + UserOverrideCode.DeleteTakeActiveDisposition, }; private UserOverrideCode(string code) diff --git a/source/backend/dal/Repositories/Interfaces/ITakeRepository.cs b/source/backend/dal/Repositories/Interfaces/ITakeRepository.cs index c76900118c..69bb5c2169 100644 --- a/source/backend/dal/Repositories/Interfaces/ITakeRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/ITakeRepository.cs @@ -10,13 +10,19 @@ public interface ITakeRepository : IRepository { IEnumerable GetAllByAcquisitionFileId(long fileId); - IEnumerable GetAllByAcqPropertyId(long fileId, long acquisitionFilePropertyId); + IEnumerable GetAllByAcqPropertyId(long fileId, long propertyId); IEnumerable GetAllByPropertyId(long propertyId); - int GetCountByPropertyId(long propertyId); + PimsTake GetById(long takeId); + + PimsTake AddTake(PimsTake take); + + PimsTake UpdateTake(PimsTake take); - void UpdateAcquisitionPropertyTakes(long acquisitionFilePropertyId, IEnumerable takes); + bool TryDeleteTake(long takeId); + + int GetCountByPropertyId(long propertyId); IEnumerable GetAllByPropertyAcquisitionFileId(long acquisitionFilePropertyId); } diff --git a/source/backend/dal/Repositories/TakeRepository.cs b/source/backend/dal/Repositories/TakeRepository.cs index c82fe18a04..a9043a53a7 100644 --- a/source/backend/dal/Repositories/TakeRepository.cs +++ b/source/backend/dal/Repositories/TakeRepository.cs @@ -24,6 +24,24 @@ public TakeRepository(PimsContext dbContext, ClaimsPrincipal user, ILogger + /// Get take by id. + /// + /// + /// + public PimsTake GetById(long takeId) + { + return Context.PimsTakes + + .Include(t => t.PropertyAcquisitionFile) + .Include(t => t.TakeSiteContamTypeCodeNavigation) + .Include(t => t.TakeStatusTypeCodeNavigation) + .Include(t => t.TakeTypeCodeNavigation) + .Include(t => t.LandActTypeCodeNavigation) + .AsNoTracking() + .FirstOrDefault(t => t.TakeId == takeId) ?? throw new KeyNotFoundException($"Unable to find take with id {takeId}"); + } + /// /// Get all of the takes that are associated to a given acquisition file by its id. /// @@ -46,9 +64,9 @@ public IEnumerable GetAllByAcquisitionFileId(long fileId) /// Get all Takes for a Property in the Acquisition File. /// /// - /// + /// /// - public IEnumerable GetAllByAcqPropertyId(long fileId, long acquisitionFilePropertyId) + public IEnumerable GetAllByAcqPropertyId(long fileId, long propertyId) { return Context.PimsTakes .Include(t => t.PropertyAcquisitionFile) @@ -57,7 +75,7 @@ public IEnumerable GetAllByAcqPropertyId(long fileId, long acquisition .Include(t => t.TakeTypeCodeNavigation) .Include(t => t.LandActTypeCodeNavigation) .Where(t => t.PropertyAcquisitionFile.AcquisitionFileId == fileId - && t.PropertyAcquisitionFile.PropertyId == acquisitionFilePropertyId) + && t.PropertyAcquisitionFile.PropertyId == propertyId) .AsNoTracking(); } @@ -87,14 +105,44 @@ public int GetCountByPropertyId(long propertyId) .Count(); } + public PimsTake AddTake(PimsTake take) + { + using var scope = Logger.QueryScope(); + + Context.PimsTakes.Add(take); + + return take; + } + /// - /// Sets the passed list of takes as the takes associated to the given acquisition property, adding, deleting and updating as necessary. + /// Update the passed take. /// - /// - /// - public void UpdateAcquisitionPropertyTakes(long acquisitionFilePropertyId, IEnumerable takes) + /// + public PimsTake UpdateTake(PimsTake take) + { + using var scope = Logger.QueryScope(); + + var existingTake = Context.PimsTakes.FirstOrDefault(x => x.TakeId == take.TakeId) ?? throw new KeyNotFoundException(); + + take.PropertyAcquisitionFileId = existingTake.PropertyAcquisitionFileId; // A take cannot be migrated between properties. + Context.Entry(existingTake).CurrentValues.SetValues(take); + + return existingTake; + } + + public bool TryDeleteTake(long takeId) { - Context.UpdateChild(p => p.PimsTakes, acquisitionFilePropertyId, takes.ToArray(), true); + using var scope = Logger.QueryScope(); + + var deletedEntity = Context.PimsTakes.Where(x => x.TakeId == takeId).FirstOrDefault(); + if (deletedEntity is not null) + { + Context.PimsTakes.Remove(deletedEntity); + + return true; + } + + return false; } public IEnumerable GetAllByPropertyAcquisitionFileId(long acquisitionFilePropertyId) diff --git a/source/backend/entities/Extensions/ExpiringTypeEntity.cs b/source/backend/entities/Extensions/ExpiringTypeEntity.cs index 12109900ca..38ff5e763f 100644 --- a/source/backend/entities/Extensions/ExpiringTypeEntity.cs +++ b/source/backend/entities/Extensions/ExpiringTypeEntity.cs @@ -24,7 +24,7 @@ public static bool IsExpiredType(this IExpiringTypeEntity t /// True if the type is expired; false otherwise. public static bool IsExpiredType(this IExpiringTypeEntity type, DateTime? currentDate = null) { - DateOnly now = DateOnly.FromDateTime(currentDate.HasValue ? (DateTime)currentDate?.Date : DateTime.UtcNow.Date); + DateOnly now = DateOnly.FromDateTime(currentDate.HasValue ? (DateTime)currentDate?.Date : DateTime.Now.Date); return (type.EffectiveDate > now) || (type.ExpiryDate.HasValue && type.ExpiryDate <= now); } } diff --git a/source/backend/tests/core/Entities/TakeHelper.cs b/source/backend/tests/core/Entities/TakeHelper.cs new file mode 100644 index 0000000000..9621a7e3cb --- /dev/null +++ b/source/backend/tests/core/Entities/TakeHelper.cs @@ -0,0 +1,40 @@ +using Pims.Dal.Entities; +using System; +using Entity = Pims.Dal.Entities; + +namespace Pims.Core.Test +{ + /// + /// EntityHelper static class, provides helper methods to create test entities. + /// + public static partial class EntityHelper + { + /// + /// Create a new instance of a Take. + /// + /// + /// + public static Entity.PimsTake CreateTake(long id = 1) + { + return new Entity.PimsTake() + { + Internal_Id = id, + TakeId = id, + AppCreateTimestamp = DateTime.Now, + AppCreateUserid = "admin", + AppCreateUserDirectory = string.Empty, + AppLastUpdateUserDirectory = string.Empty, + AppLastUpdateUserid = string.Empty, + DbCreateUserid = string.Empty, + DbLastUpdateUserid = string.Empty, + ConcurrencyControlNumber = 1, + AreaUnitTypeCodeNavigation = new Entity.PimsAreaUnitType() { Id = id.ToString(), DbCreateUserid = "test", DbLastUpdateUserid = "test", AreaUnitTypeCode = "test", Description = "test" }, + LandActTypeCodeNavigation = new Entity.PimsLandActType() { Id = id.ToString(), DbCreateUserid = "test", DbLastUpdateUserid = "test", LandActTypeCode = "test", Description = "test" }, + TakeSiteContamTypeCodeNavigation = new PimsTakeSiteContamType() { Id = id.ToString(), DbCreateUserid = "test", DbLastUpdateUserid = "test", TakeSiteContamTypeCode = "test", Description = "test" }, + TakeStatusTypeCodeNavigation = new PimsTakeStatusType() { Id = id.ToString(), DbCreateUserid = "test", DbLastUpdateUserid = "test", TakeStatusTypeCode = "test", Description = "test" }, + TakeTypeCodeNavigation = new PimsTakeType() { Id = id.ToString(), DbCreateUserid = "test", DbLastUpdateUserid = "test", TakeTypeCode = "test", Description = "test" }, + PropertyAcquisitionFile = new PimsPropertyAcquisitionFile(), + }; + } + } +} diff --git a/source/backend/tests/unit/api/Controllers/Takes/TakeControllerTest.cs b/source/backend/tests/unit/api/Controllers/Takes/TakeControllerTest.cs new file mode 100644 index 0000000000..55bafd40c6 --- /dev/null +++ b/source/backend/tests/unit/api/Controllers/Takes/TakeControllerTest.cs @@ -0,0 +1,221 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using FluentAssertions; +using MapsterMapper; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Pims.Api.Areas.Admin.Controllers; +using Pims.Api.Areas.Takes.Controllers; +using Pims.Api.Models.Concepts.Take; +using Pims.Api.Models.Concepts; +using Pims.Api.Services; +using Pims.Core.Exceptions; +using Pims.Core.Test; +using Pims.Dal.Entities; +using Pims.Dal.Security; +using Xunit; +using System; +using Pims.Api.Helpers.Exceptions; +using Pims.Dal.Exceptions; + +namespace Pims.Api.Test.Controllers +{ + [Trait("category", "unit")] + [Trait("category", "api")] + [Trait("area", "takes")] + [ExcludeFromCodeCoverage] + public class TakeControllerTest + { + // xUnit.net creates a new instance of the test class for every test that is run, + // so any code which is placed into the constructor of the test class will be run for every single test. + private readonly TestHelper _helper; + private readonly TakeController _controller; + private readonly Mock _service; + private readonly IMapper _mapper; + + public TakeControllerTest() + { + this._helper = new TestHelper(); + this._controller = this._helper.CreateController(Permissions.PropertyView, Permissions.AcquisitionFileView); + this._service = this._helper.GetService>(); + this._mapper = this._helper.GetService(); + } + + [Fact] + public void GetTakesByAcquisitionFileId_Success() + { + // Arrange + this._service.Setup(m => m.GetByFileId(It.IsAny())); + + // Act + var result = this._controller.GetTakesByAcquisitionFileId(1); + + // Assert + result.Should().BeOfType(); + this._service.Verify(m => m.GetByFileId(It.IsAny()), Times.Once()); + } + + [Fact] + public void GetTakesByPropertyId_Success() + { + // Arrange + this._service.Setup(m => m.GetByPropertyId(It.IsAny(), It.IsAny())); + + // Act + var result = this._controller.GetTakesByPropertyId(1, 2); + + // Assert + result.Should().BeOfType(); + this._service.Verify(m => m.GetByPropertyId(It.IsAny(), It.IsAny()), Times.Once()); + } + + [Fact] + public void GetTakesCountByPropertyId_Success() + { + // Arrange + this._service.Setup(m => m.GetCountByPropertyId(It.IsAny())); + + // Act + var result = this._controller.GetTakesCountByPropertyId(1); + + // Assert + result.Should().BeOfType(); + this._service.Verify(m => m.GetCountByPropertyId(It.IsAny()), Times.Once()); + } + + [Fact] + public void GetTakeById_Success() + { + // Arrange + this._service.Setup(m => m.GetById(It.IsAny())).Returns(new PimsTake() { PropertyAcquisitionFileId = 1 }); + + // Act + var result = this._controller.GetTakeByPropertyFileId(1, 1); + + // Assert + result.Should().BeOfType(); + this._service.Verify(m => m.GetById(It.IsAny())); + } + + [Fact] + public void GetTakeById_InvalidId() + { + // Arrange + this._service.Setup(m => m.GetById(It.IsAny())).Returns(new PimsTake() { PropertyAcquisitionFileId = 2 }); + + // Act + Action act = () => this._controller.GetTakeByPropertyFileId(1, 1); + + // Assert + act.Should().Throw().WithMessage("Invalid acquisition file property id."); + } + + [Fact] + public void UpdateTakeByPropertyId_InvalidFilePropertyId() + { + // Arrange + this._service.Setup(m => m.UpdateAcquisitionPropertyTake(It.IsAny(), It.IsAny())); + + // Act + Action act = () => this._controller.UpdateAcquisitionPropertyTake(1, 1, new TakeModel() { Id = 1 }); + + // Assert + act.Should().Throw().WithMessage("Invalid acquisition file property id."); + } + + [Fact] + public void UpdateTakeByPropertyId_InvalidTakeId() + { + // Arrange + this._service.Setup(m => m.UpdateAcquisitionPropertyTake(It.IsAny(), It.IsAny())); + + // Act + Action act = () => this._controller.UpdateAcquisitionPropertyTake(1, 1, new TakeModel() { PropertyAcquisitionFileId = 1 }); + + // Assert + act.Should().Throw().WithMessage("Invalid take id."); + } + + [Fact] + public void UpdateTakeByPropertyId_Success() + { + // Arrange + this._service.Setup(m => m.UpdateAcquisitionPropertyTake(It.IsAny(), It.IsAny())); + + // Act + var result = this._controller.UpdateAcquisitionPropertyTake(1, 1, new TakeModel() { PropertyAcquisitionFileId = 1, Id = 1 }); + + // Assert + result.Should().BeOfType(); + this._service.Verify(m => m.UpdateAcquisitionPropertyTake(It.IsAny(), It.IsAny())); + } + + [Fact] + public void DeleteTakeByPropertyId_Invalid_AcquisitionFilePropertyId() + { + // Arrange + this._service.Setup(m => m.GetById(It.IsAny())).Returns(new PimsTake() { PropertyAcquisitionFileId = 2 }); + + // Act + Action act = () => this._controller.DeleteAcquisitionPropertyTake(1, 1, new string[0]); + + // Assert + act.Should().Throw().WithMessage("Invalid acquisition file property id."); + } + + [Fact] + public void DeleteTakeByPropertyId_Failed_Delete() + { + // Arrange + this._service.Setup(m => m.GetById(It.IsAny())).Returns(new PimsTake() { PropertyAcquisitionFileId = 1 }); + this._service.Setup(m => m.DeleteAcquisitionPropertyTake(It.IsAny(), It.IsAny>())).Returns(false); + + // Act + Action act = () => this._controller.DeleteAcquisitionPropertyTake(1, 1, new string[0]); + + // Assert + act.Should().Throw().WithMessage("Failed to delete take 1."); + } + + [Fact] + public void DeleteTakeByPropertyId_Success() + { + // Arrange + this._service.Setup(m => m.GetById(It.IsAny())).Returns(new PimsTake() { PropertyAcquisitionFileId = 1 }); + this._service.Setup(m => m.DeleteAcquisitionPropertyTake(It.IsAny(), It.IsAny>())).Returns(true); + + // Act + this._controller.DeleteAcquisitionPropertyTake(1, 1, new string[0]); + + // Assert + this._service.Verify(m => m.DeleteAcquisitionPropertyTake(It.IsAny(), It.IsAny>())); + } + + [Fact] + public void AddTakeByPropertyId_InvalidId() + { + // Arrange + this._service.Setup(m => m.AddAcquisitionPropertyTake(It.IsAny(), It.IsAny())); + + // Act + Action act = () => this._controller.AddAcquisitionPropertyTake(1, new TakeModel()); + + // Assert + act.Should().Throw().WithMessage("Invalid acquisition file property id."); + } + + [Fact] + public void AddTakeByPropertyId_Success() + { + // Arrange + this._service.Setup(m => m.AddAcquisitionPropertyTake(It.IsAny(), It.IsAny())); + + // Act + var result = this._controller.AddAcquisitionPropertyTake(1, new TakeModel() { PropertyAcquisitionFileId = 1 }); + + // Assert + result.Should().BeOfType(); + this._service.Verify(m => m.AddAcquisitionPropertyTake(It.IsAny(), It.IsAny())); + } + } +} diff --git a/source/backend/tests/unit/api/Controllers/Takes/TakesControllerTest.cs b/source/backend/tests/unit/api/Controllers/Takes/TakesControllerTest.cs deleted file mode 100644 index 5767b9feb9..0000000000 --- a/source/backend/tests/unit/api/Controllers/Takes/TakesControllerTest.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using FluentAssertions; -using MapsterMapper; -using Microsoft.AspNetCore.Mvc; -using Moq; -using Pims.Api.Areas.Admin.Controllers; -using Pims.Api.Areas.Takes.Controllers; -using Pims.Api.Models.Concepts.Take; -using Pims.Api.Models.Concepts; -using Pims.Api.Services; -using Pims.Core.Exceptions; -using Pims.Core.Test; -using Pims.Dal.Entities; -using Pims.Dal.Security; -using Xunit; - -namespace Pims.Api.Test.Controllers -{ - [Trait("category", "unit")] - [Trait("category", "api")] - [Trait("area", "takes")] - [ExcludeFromCodeCoverage] - public class TakesControllerTest - { - // xUnit.net creates a new instance of the test class for every test that is run, - // so any code which is placed into the constructor of the test class will be run for every single test. - private readonly TestHelper _helper; - private readonly TakeController _controller; - private readonly Mock _service; - private readonly IMapper _mapper; - - public TakesControllerTest() - { - this._helper = new TestHelper(); - this._controller = this._helper.CreateController(Permissions.PropertyView, Permissions.AcquisitionFileView); - this._service = this._helper.GetService>(); - this._mapper = this._helper.GetService(); - } - - [Fact] - public void GetTakesByAcquisitionFileId_Success() - { - // Arrange - this._service.Setup(m => m.GetByFileId(It.IsAny())); - - // Act - var result = this._controller.GetTakesByAcquisitionFileId(1); - - // Assert - result.Should().BeOfType(); - this._service.Verify(m => m.GetByFileId(It.IsAny()), Times.Once()); - } - - [Fact] - public void GetTakesByPropertyId_Success() - { - // Arrange - this._service.Setup(m => m.GetByPropertyId(It.IsAny(), It.IsAny())); - - // Act - var result = this._controller.GetTakesByPropertyId(1, 2); - - // Assert - result.Should().BeOfType(); - this._service.Verify(m => m.GetByPropertyId(It.IsAny(), It.IsAny()), Times.Once()); - } - - [Fact] - public void GetTakesCountByPropertyId_Success() - { - // Arrange - this._service.Setup(m => m.GetCountByPropertyId(It.IsAny())); - - // Act - var result = this._controller.GetTakesCountByPropertyId(1); - - // Assert - result.Should().BeOfType(); - this._service.Verify(m => m.GetCountByPropertyId(It.IsAny()), Times.Once()); - } - - [Fact] - public void UpdateAcquisitionPropertyTakes_Success() - { - // Arrange - this._service.Setup(m => m.UpdateAcquisitionPropertyTakes(It.IsAny(), It.IsAny>())); - - // Act - var result = this._controller.UpdateAcquisitionPropertyTakes(1, new List()); - - // Assert - result.Should().BeOfType(); - this._service.Verify(m => m.UpdateAcquisitionPropertyTakes(It.IsAny(), It.IsAny>())); - } - } -} diff --git a/source/backend/tests/unit/api/Services/TakeServiceTest.cs b/source/backend/tests/unit/api/Services/TakeServiceTest.cs index b0f7233ed4..7e18042639 100644 --- a/source/backend/tests/unit/api/Services/TakeServiceTest.cs +++ b/source/backend/tests/unit/api/Services/TakeServiceTest.cs @@ -38,6 +38,34 @@ private TakeService CreateWithPermissions(params Permissions[] permissions) return this._helper.Create(user); } + [Fact] + public void GetById_Success() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + var repo = this._helper.GetService>(); + repo.Setup(x => x.GetById(It.IsAny())); + + // Act + var result = service.GetById(1); + + // Assert + repo.Verify(x => x.GetById(It.IsAny()), Times.Once); + } + + [Fact] + public void GetById_NoPermission() + { + // Arrange + var service = this.CreateWithPermissions(); + + // Act + Action act = () => service.GetById(1); + + // Assert + act.Should().Throw(); + } + [Fact] public void GetByFileId_Success() { @@ -124,13 +152,13 @@ public void GetCountByPropertyId_NoPermission() } [Fact] - public void Update_Success() + public void Add_Success() { // Arrange var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); var takeRepository = this._helper.GetService>(); takeRepository.Setup(x => - x.UpdateAcquisitionPropertyTakes(It.IsAny(), It.IsAny>())); + x.AddTake(It.IsAny())); var acqRepository = this._helper.GetService>(); acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); @@ -140,20 +168,231 @@ public void Update_Success() solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); // Act - var result = service.UpdateAcquisitionPropertyTakes(1, new List()); + var result = service.AddAcquisitionPropertyTake(1, new PimsTake()); + + // Assert + takeRepository.Verify(x => x.AddTake(It.IsAny()), Times.Once); + } + + [Fact] + public void Add_NoPermission() + { + // Arrange + var service = this.CreateWithPermissions(); + + // Act + Action act = () => service.AddAcquisitionPropertyTake(1, new PimsTake()); // Assert - takeRepository.Verify(x => x.UpdateAcquisitionPropertyTakes(1, new List()), Times.Once); + act.Should().Throw(); } [Fact] - public void Update_TakeComplete_No_Date() + public void Add_InvalidStatus_AcquisitionFile_Complete() { // Arrange var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); var takeRepository = this._helper.GetService>(); takeRepository.Setup(x => - x.UpdateAcquisitionPropertyTakes(It.IsAny(), It.IsAny>())); + x.AddTake(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.AddAcquisitionPropertyTake(1, new PimsTake()); + + // Assert + act.Should().Throw().WithMessage("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion."); + } + + [Fact] + public void Add_CompleteTake_No_Date() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + } + ); + + PimsTake completedTake = new() + { + TakeId = 100, + TakeStatusTypeCode = AcquisitionTakeStatusTypes.COMPLETE.ToString(), + }; + + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => x.GetAllByPropertyAcquisitionFileId(It.IsAny())).Returns( + new List() { completedTake } + ); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + + // Act + Action act = () => service.AddAcquisitionPropertyTake(1, completedTake); + + // Assert + act.Should().Throw().WithMessage("A completed take must have a completion date."); + } + + [Fact] + public void Add_CompleteTake_LandActType_No_EndDt() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + } + ); + + PimsTake completedTake = new() + { + TakeId = 100, + CompletionDt = DateOnly.FromDateTime(DateTime.Now), + TakeStatusTypeCode = AcquisitionTakeStatusTypes.COMPLETE.ToString(), + IsNewLandAct = true, + LandActTypeCode = LandActTypes.TRANSFER_OF_ADMIN_AND_CONTROL.ToString(), + LandActEndDt = DateOnly.FromDateTime(DateTime.Now), + }; + + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => x.GetAllByPropertyAcquisitionFileId(It.IsAny())).Returns( + new List() { completedTake } + ); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + + // Act + Action act = () => service.AddAcquisitionPropertyTake(1, completedTake); + + // Assert + act.Should().Throw().WithMessage("'Crown Grant' and 'Transfer' Land Acts cannot have an end date."); + } + + [Fact] + public void Add_AcquisitionFile_Active_Update_CompleteTake_Admin_Success() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.SystemAdmin, Permissions.PropertyView, Permissions.AcquisitionFileView); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetProperty(It.IsAny())).Returns(new PimsProperty() { PropertyId = 1 }); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); + + PimsTake completedTake = new() + { + TakeId = 100, + TakeStatusTypeCode = AcquisitionTakeStatusTypes.COMPLETE.ToString(), + }; + + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns(new List() { completedTake }); + takeRepository.Setup(x => x.UpdateTake(It.IsAny())).Returns(completedTake); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + + // Act + var result = service.AddAcquisitionPropertyTake(1, new PimsTake()); + + // Assert + takeRepository.Verify(x => x.AddTake(It.IsAny()), Times.Once); + } + + [Fact] + public void Add_AcquisitionFile_Active_Update_CompleteTake_Error() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetProperty(It.IsAny())).Returns(new PimsProperty() { PropertyId = 1 }); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); + + PimsTake completedTake = new() + { + TakeId = 100, + TakeStatusTypeCode = AcquisitionTakeStatusTypes.COMPLETE.ToString(), + }; + + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns(new List() { completedTake }); + takeRepository.Setup(x => x.GetAllByPropertyAcquisitionFileId(It.IsAny())).Returns(new List() { completedTake }); + takeRepository.Setup(x => x.UpdateTake(It.IsAny())).Returns(completedTake); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + + // Act + Action act = () => service.AddAcquisitionPropertyTake(1, new PimsTake() { TakeId = 100 }); + + // Assert + act.Should().Throw(); + } + + public static IEnumerable takesAddTestParameters = new List() { + //new object[] { new List(), false }, // No takes should be core inventory + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="CANCELLED" }}, false, false }, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="INPROGRESS" }}, false , false}, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE", CompletionDt = new DateOnly() }}, true, true }, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE", CompletionDt = new DateOnly() }}, false , false}, + }.ToArray(); + + [Theory, MemberData(nameof(takesAddTestParameters))] + public void Add_Success_Transfer_MultipleTakes_Core(List takes, bool solverResult, bool expectTransfer) + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => + x.UpdateTake(It.IsAny())); + takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns(new List()); + + var takeInteractionSolver = this._helper.GetService>(); + takeInteractionSolver.Setup(x => x.ResultsInOwnedProperty(It.IsAny>())).Returns(solverResult); + + var propertyRepository = this._helper.GetService>(); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetProperty(It.IsAny())).Returns(new PimsProperty() { PropertyId = 1 }); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); + + var acqStatusSolver = this._helper.GetService>(); + acqStatusSolver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + + // Act + var result = service.AddAcquisitionPropertyTake(1, takes.FirstOrDefault()); + + var completedCount = takes.Count(x => x.TakeStatusTypeCode == "COMPLETE"); + + // Assert + takeRepository.Verify(x => x.AddTake(takes.FirstOrDefault()), Times.Once); + takeInteractionSolver.Verify(x => x.ResultsInOwnedProperty(It.IsAny>()), completedCount > 0 ? Times.Once : Times.Never); + propertyRepository.Verify(x => x.TransferFileProperty(It.IsAny(), true), expectTransfer ? Times.Once : Times.Never); + } + + [Fact] + public void Update_Success() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => + x.UpdateTake(It.IsAny())); var acqRepository = this._helper.GetService>(); acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); @@ -163,10 +402,10 @@ public void Update_TakeComplete_No_Date() solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); // Act - var result = service.UpdateAcquisitionPropertyTakes(1, new List()); + var result = service.UpdateAcquisitionPropertyTake(1, new PimsTake()); // Assert - takeRepository.Verify(x => x.UpdateAcquisitionPropertyTakes(1, new List()), Times.Once); + takeRepository.Verify(x => x.UpdateTake(It.IsAny()), Times.Once); } [Fact] @@ -176,7 +415,7 @@ public void Update_NoPermission() var service = this.CreateWithPermissions(); // Act - Action act = () => service.UpdateAcquisitionPropertyTakes(1, new List()); + Action act = () => service.UpdateAcquisitionPropertyTake(1, new PimsTake()); // Assert act.Should().Throw(); @@ -189,7 +428,7 @@ public void Update_InvalidStatus_AcquisitionFile_Complete() var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); var takeRepository = this._helper.GetService>(); takeRepository.Setup(x => - x.UpdateAcquisitionPropertyTakes(It.IsAny(), It.IsAny>())); + x.UpdateTake(It.IsAny())); var acqRepository = this._helper.GetService>(); acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); @@ -198,7 +437,7 @@ public void Update_InvalidStatus_AcquisitionFile_Complete() solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(false); // Act - Action act = () => service.UpdateAcquisitionPropertyTakes(1, new List()); + Action act = () => service.UpdateAcquisitionPropertyTake(1, new PimsTake()); // Assert act.Should().Throw().WithMessage("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion."); @@ -233,7 +472,7 @@ public void Update_CompleteTake_No_Date() solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); // Act - Action act = () => service.UpdateAcquisitionPropertyTakes(1, new List() { completedTake }); + Action act = () => service.UpdateAcquisitionPropertyTake(1, completedTake); // Assert act.Should().Throw().WithMessage("A completed take must have a completion date."); @@ -272,24 +511,21 @@ public void Update_CompleteTake_LandActType_No_EndDt() solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); // Act - Action act = () => service.UpdateAcquisitionPropertyTakes(1, new List() { completedTake }); + Action act = () => service.UpdateAcquisitionPropertyTake(1, completedTake); // Assert act.Should().Throw().WithMessage("'Crown Grant' and 'Transfer' Land Acts cannot have an end date."); } [Fact] - public void Update_InvalidStatus_AcquisitionFile_Active_DeleteCompleteTake_NotAdmin() + public void Update_AcquisitionFile_Active_Update_CompleteTake_Admin_Success() { // Arrange - var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + var service = this.CreateWithPermissions(Permissions.SystemAdmin, Permissions.PropertyView, Permissions.AcquisitionFileView); var acqRepository = this._helper.GetService>(); - acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns( - new PimsAcquisitionFile() { - AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() - } - ); + acqRepository.Setup(x => x.GetProperty(It.IsAny())).Returns(new PimsProperty() { PropertyId = 1 }); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() }); PimsTake completedTake = new() { @@ -298,25 +534,25 @@ public void Update_InvalidStatus_AcquisitionFile_Active_DeleteCompleteTake_NotAd }; var takeRepository = this._helper.GetService>(); - takeRepository.Setup(x => x.GetAllByPropertyAcquisitionFileId(It.IsAny())).Returns( - new List() { completedTake } - ); + takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns(new List() { completedTake }); + takeRepository.Setup(x => x.UpdateTake(It.IsAny())).Returns(completedTake); var solver = this._helper.GetService>(); solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); // Act - Action act = () => service.UpdateAcquisitionPropertyTakes(1, new List()); + var result = service.UpdateAcquisitionPropertyTake(1, new PimsTake()); // Assert - act.Should().Throw().WithMessage("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion."); + Assert.NotNull(result); + takeRepository.Verify(x => x.UpdateTake(It.IsAny()), Times.Once); } [Fact] - public void Update_AcquisitionFile_Active_DeleteCompleteTake_Admin_Success() + public void Update_AcquisitionFile_Active_Update_CompleteTake_Error() { // Arrange - var service = this.CreateWithPermissions(Permissions.SystemAdmin, Permissions.PropertyView, Permissions.AcquisitionFileView); + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); var acqRepository = this._helper.GetService>(); acqRepository.Setup(x => x.GetProperty(It.IsAny())).Returns(new PimsProperty() { PropertyId = 1 }); @@ -330,17 +566,17 @@ public void Update_AcquisitionFile_Active_DeleteCompleteTake_Admin_Success() var takeRepository = this._helper.GetService>(); takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns(new List() { completedTake }); - //takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns(takes); + takeRepository.Setup(x => x.GetAllByPropertyAcquisitionFileId(It.IsAny())).Returns(new List() { completedTake }); + takeRepository.Setup(x => x.UpdateTake(It.IsAny())).Returns(completedTake); var solver = this._helper.GetService>(); solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); // Act - var result = service.UpdateAcquisitionPropertyTakes(1, new List()); + Action act = () => service.UpdateAcquisitionPropertyTake(1, new PimsTake() { TakeId = 100 }); // Assert - Assert.NotNull(result); - takeRepository.Verify(x => x.UpdateAcquisitionPropertyTakes(1, new List()), Times.Once); + act.Should().Throw(); } public static IEnumerable takesTestParameters = new List() { @@ -358,10 +594,10 @@ public void Update_Success_Transfer_MultipleTakes_Core(List takes, boo var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); var takeRepository = this._helper.GetService>(); takeRepository.Setup(x => - x.UpdateAcquisitionPropertyTakes(It.IsAny(), It.IsAny>())); - takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns(takes); + x.UpdateTake(It.IsAny())); + takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns(new List()); - var takeInteractionSolver = this._helper.GetMock(); + var takeInteractionSolver = this._helper.GetService>(); takeInteractionSolver.Setup(x => x.ResultsInOwnedProperty(It.IsAny>())).Returns(solverResult); var propertyRepository = this._helper.GetService>(); @@ -374,14 +610,346 @@ public void Update_Success_Transfer_MultipleTakes_Core(List takes, boo acqStatusSolver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); // Act - var result = service.UpdateAcquisitionPropertyTakes(1, takes); + var result = service.UpdateAcquisitionPropertyTake(1, takes.FirstOrDefault()); var completedCount = takes.Count(x => x.TakeStatusTypeCode == "COMPLETE"); // Assert - takeRepository.Verify(x => x.UpdateAcquisitionPropertyTakes(1, takes), Times.Once); - takeInteractionSolver.Verify(x => x.ResultsInOwnedProperty(takes), completedCount > 0 ? Times.Once : Times.Never); + takeRepository.Verify(x => x.UpdateTake(takes.FirstOrDefault()), Times.Once); + takeInteractionSolver.Verify(x => x.ResultsInOwnedProperty(It.IsAny>()), completedCount > 0 ? Times.Once : Times.Never); propertyRepository.Verify(x => x.TransferFileProperty(It.IsAny(), true), expectTransfer ? Times.Once : Times.Never); } + + [Fact] + public void Delete_NoPermission() + { + // Arrange + var service = this.CreateWithPermissions(); + + // Act + Action act = () => service.DeleteAcquisitionPropertyTake(1, new List()); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Delete_AcquisitionFile_Active_DeleteCompleteTake_Success() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView, Permissions.SystemAdmin); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + } + ); + acqRepository.Setup(x => x.GetProperty(It.IsAny())).Returns( + EntityHelper.CreateProperty(1) + ); + + PimsTake completedTake = new() + { + TakeId = 100, + TakeStatusTypeCode = AcquisitionTakeStatusTypes.COMPLETE.ToString(), + PropertyAcquisitionFile = new PimsPropertyAcquisitionFile(), + }; + + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => x.GetById(It.IsAny())).Returns( + completedTake + ); + takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns(new List() { + completedTake + } + ); + takeRepository.Setup(x => x.TryDeleteTake(It.IsAny())).Returns(true); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + + var propertyRepository = this._helper.GetService>(); + + // Act + var deleted = service.DeleteAcquisitionPropertyTake(1, new List() { UserOverrideCode.DeleteCompletedTake }); + + // Assert + deleted.Should().BeTrue(); + propertyRepository.Verify(x => x.TransferFileProperty(It.IsAny(), false), Times.Once); + } + + [Fact] + public void Delete_AcquisitionFile_Active_DeleteCompleteTake_Success_Owned() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView, Permissions.SystemAdmin); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + } + ); + acqRepository.Setup(x => x.GetProperty(It.IsAny())).Returns( + EntityHelper.CreateProperty(1) + ); + + PimsTake completedTake = new() + { + TakeId = 100, + TakeStatusTypeCode = AcquisitionTakeStatusTypes.COMPLETE.ToString(), + PropertyAcquisitionFile = new PimsPropertyAcquisitionFile(), + }; + + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => x.GetById(It.IsAny())).Returns( + completedTake + ); + takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns(new List() { + completedTake + } + ); + takeRepository.Setup(x => x.TryDeleteTake(It.IsAny())).Returns(true); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + + var takeSolver = this._helper.GetService>(); + takeSolver.Setup(x => x.ResultsInOwnedProperty(It.IsAny>())).Returns(true); + + var propertyRepository = this._helper.GetService>(); + + // Act + var deleted = service.DeleteAcquisitionPropertyTake(1, new List() { UserOverrideCode.DeleteCompletedTake }); + + // Assert + deleted.Should().BeTrue(); + propertyRepository.Verify(x => x.TransferFileProperty(It.IsAny(), true), Times.Once); + } + + [Fact] + public void Delete_AcquisitionFile_Active_DeleteCompleteTake_NotAdmin() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + } + ); + + PimsTake completedTake = new() + { + TakeId = 100, + TakeStatusTypeCode = AcquisitionTakeStatusTypes.COMPLETE.ToString(), + PropertyAcquisitionFile = new PimsPropertyAcquisitionFile(), + }; + + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => x.GetById(It.IsAny())).Returns( + completedTake + ); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + + // Act + Action act = () => service.DeleteAcquisitionPropertyTake(1, new List()); + + // Assert + act.Should().Throw().WithMessage("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion."); + } + + [Fact] + public void Delete_AcquisitionFile_Active_DeleteTake_CompleteFile_NotAdmin() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + } + ); + + PimsTake completedTake = new() + { + TakeId = 100, + TakeStatusTypeCode = AcquisitionTakeStatusTypes.INPROGRESS.ToString(), + PropertyAcquisitionFile = new PimsPropertyAcquisitionFile(), + }; + + var takeRepository = this._helper.GetService>(); + takeRepository.Setup(x => x.GetById(It.IsAny())).Returns( + completedTake + ); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(false); + + // Act + Action act = () => service.DeleteAcquisitionPropertyTake(1, new List()); + + // Assert + act.Should().Throw().WithMessage("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion."); + } + + [Fact] + public void Delete_AcquisitionFile_Active_DeleteTake_IsRetired() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + } + ); + + PimsTake completedTake = new() + { + TakeId = 100, + TakeStatusTypeCode = AcquisitionTakeStatusTypes.INPROGRESS.ToString(), + }; + + var propertyRepository = this._helper.GetService>(); + var property = EntityHelper.CreateProperty(1); + property.IsRetired = true; + propertyRepository.Setup(x => x.GetAllAssociationsById(It.IsAny())).Returns(property); + + var takeRepository = this._helper.GetService>(); + completedTake.PropertyAcquisitionFile = new PimsPropertyAcquisitionFile() { Property = property }; + takeRepository.Setup(x => x.GetById(It.IsAny())).Returns( + completedTake + ); + takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns( + new List() { completedTake } + ); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + + // Act + Action act = () => service.DeleteAcquisitionPropertyTake(1, new List()); + + // Assert + act.Should().Throw().WithMessage("You cannot delete a take from a retired property."); + } + + [Fact] + public void Delete_InvalidStatus_AcquisitionFile_Active_DeleteTake_CompleteDisposition() + { + // Arrange + var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + } + ); + + PimsTake completedTake = new() + { + TakeId = 100, + TakeStatusTypeCode = AcquisitionTakeStatusTypes.INPROGRESS.ToString(), + }; + + var propertyRepository = this._helper.GetService>(); + var property = EntityHelper.CreateProperty(1); + var dispFile = EntityHelper.CreateDispositionFile(); + dispFile.DispositionFileStatusTypeCode = DispositionFileStatusTypes.COMPLETE.ToString(); + property.PimsDispositionFileProperties = new List() { new PimsDispositionFileProperty() { DispositionFile = dispFile } }; + propertyRepository.Setup(x => x.GetAllAssociationsById(It.IsAny())).Returns(property); + + var takeRepository = this._helper.GetService>(); + completedTake.PropertyAcquisitionFile = new PimsPropertyAcquisitionFile() { Property = property }; + takeRepository.Setup(x => x.GetById(It.IsAny())).Returns( + completedTake + ); + takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns( + new List() { completedTake } + ); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(true); + + // Act + Action act = () => service.DeleteAcquisitionPropertyTake(1, new List()); + + // Assert + act.Should().Throw().WithMessage("You cannot delete a take that has a completed disposition attached to the same property."); + } + + public static IEnumerable deleteTakeTestParameters = new List() { + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE" }}, UserOverrideCode.DeleteTakeActiveDisposition, true, new List() }, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE" }}, UserOverrideCode.DeleteCompletedTake, false, new List()}, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE", TakeId = 1, }}, UserOverrideCode.DeleteLastTake, false, new List()}, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE", TakeId = 1, }, new PimsTake() { TakeStatusTypeCode = "ACTIVE", TakeId = 2, } }, UserOverrideCode.DeleteLastTake, false, new List()}, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE", TakeId = 1, }, new PimsTake() { TakeStatusTypeCode = "COMPLETE", IsNewLicenseToConstruct = true, LtcEndDt = DateOnly.FromDateTime(DateTime.Now.AddDays(1)), TakeId = 2, } }, UserOverrideCode.DeleteLastTake, false, new List()}, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE", TakeId = 1, }, new PimsTake() { TakeStatusTypeCode = "COMPLETE", TakeId = 2, } }, UserOverrideCode.DeleteCompletedTake, false, new List()}, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE" }}, null, true, new List() { UserOverrideCode.DeleteTakeActiveDisposition } }, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE" }}, null, false, new List() { UserOverrideCode.DeleteCompletedTake } }, + new object[] { new List() { new PimsTake() { TakeStatusTypeCode="COMPLETE", TakeId = 1, }}, null, false, new List() { UserOverrideCode.DeleteLastTake } }, + }.ToArray(); + [Theory, MemberData(nameof(deleteTakeTestParameters))] + public void Delete_UserOverride(List takes, UserOverrideCode expectedOverride, bool hasDisposition, List userOverrideCodes) + { + // Arrange + var service = this.CreateWithPermissions(Permissions.SystemAdmin, Permissions.AcquisitionFileView); + + var acqRepository = this._helper.GetService>(); + acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny())).Returns( + new PimsAcquisitionFile() + { + AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() + } + ); + + var propertyRepository = this._helper.GetService>(); + var property = EntityHelper.CreateProperty(1); + if (hasDisposition) + { + var dispFile = EntityHelper.CreateDispositionFile(); + property.PimsDispositionFileProperties = new List() { new PimsDispositionFileProperty() { DispositionFile = dispFile } }; + } + propertyRepository.Setup(x => x.GetAllAssociationsById(It.IsAny())).Returns(property); + + var takeRepository = this._helper.GetService>(); + takes.FirstOrDefault().PropertyAcquisitionFile = new PimsPropertyAcquisitionFile() { Property = property }; + takeRepository.Setup(x => x.GetById(It.IsAny())).Returns( + takes.FirstOrDefault() + ); + takeRepository.Setup(x => x.GetAllByPropertyId(It.IsAny())).Returns( + takes + ); + + var solver = this._helper.GetService>(); + solver.Setup(x => x.CanEditTakes(It.IsAny())).Returns(false); + + // Act + Action act = () => service.DeleteAcquisitionPropertyTake(1, userOverrideCodes); + + // Assert + if (expectedOverride != null) + { + var exception = act.Should().Throw().Which; + exception.UserOverride.Should().Be(expectedOverride); + } else + { + var result = act.Should().NotThrow(); + } + } } } diff --git a/source/backend/tests/unit/dal/Repositories/TakeRepositoryTest.cs b/source/backend/tests/unit/dal/Repositories/TakeRepositoryTest.cs new file mode 100644 index 0000000000..8e75e3242b --- /dev/null +++ b/source/backend/tests/unit/dal/Repositories/TakeRepositoryTest.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using FluentAssertions; +using Pims.Api.Models.CodeTypes; +using Pims.Core.Extensions; +using Pims.Core.Test; +using Pims.Dal.Entities; +using Pims.Dal.Entities.Models; +using Pims.Dal.Exceptions; +using Pims.Dal.Repositories; +using Pims.Dal.Security; +using Xunit; +using Entity = Pims.Dal.Entities; + +namespace Pims.Dal.Test.Repositories +{ + [Trait("category", "unit")] + [Trait("category", "dal")] + [Trait("group", "property")] + [ExcludeFromCodeCoverage] + public class TakeRepositoryTest + { + private TestHelper _helper; + + public TakeRepositoryTest() + { + _helper = new TestHelper(); + } + + private TakeRepository CreateRepositoryWithPermissions(params Permissions[] permissions) + { + var user = PrincipalHelper.CreateForPermission(permissions); + _helper.CreatePimsContext(user, true); + return _helper.CreateRepository(); + } + + #region Tests + + + [Fact] + public void GetById_Throw_KeyNotFoundException() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + + // Act + Action act = () => repository.GetById(9999); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void GetById_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + _helper.AddAndSaveChanges(EntityHelper.CreateTake(9999)); + + // Act + var result = repository.GetById(9999); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public void GetAllByAcquisitionFileId_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + take.PropertyAcquisitionFile.AcquisitionFileId = 5; + _helper.AddAndSaveChanges(take); + + // Act + var result = repository.GetAllByAcquisitionFileId(5); + + // Assert + result.Should().NotBeEmpty(); + } + + [Fact] + public void GetAllByAcquisitionFilePropertyId_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + take.PropertyAcquisitionFile.AcquisitionFileId = 5; + take.PropertyAcquisitionFile.PropertyId = 10; + _helper.AddAndSaveChanges(take); + + // Act + var result = repository.GetAllByAcqPropertyId(5, 10); + + // Assert + result.Should().NotBeEmpty(); + } + + [Fact] + public void GetAllByPropertyAcquisitionFileId_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + _helper.AddAndSaveChanges(take); + + // Act + var result = repository.GetAllByPropertyAcquisitionFileId(1); + + // Assert + result.Should().NotBeEmpty(); + } + + [Fact] + public void GetAllByPropertyId_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + take.PropertyAcquisitionFile.PropertyId = 10; + _helper.AddAndSaveChanges(take); + + // Act + var result = repository.GetAllByPropertyId(10); + + // Assert + result.Should().NotBeEmpty(); + } + + [Fact] + public void GetCountByPropertyId_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + take.PropertyAcquisitionFile.PropertyId = 10; + _helper.AddAndSaveChanges(take); + + // Act + var result = repository.GetCountByPropertyId(10); + + // Assert + result.Should().Be(1); + } + + [Fact] + public void AddTake_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + + // Act + var result = repository.AddTake(take); + + // Assert + result.TakeId.Should().Be(9999); + } + + [Fact] + public void UpdateTake_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + take.PropertyAcquisitionFile.PropertyId = 10; + _helper.AddAndSaveChanges(take); + + var newTake = EntityHelper.CreateTake(9999); + take.IsAcquiredForInventory = true; + + // Act + var result = repository.UpdateTake(take); + + // Assert + result.IsAcquiredForInventory.Should().BeTrue(); + } + + [Fact] + public void UpdateTake_Success_PropertyAcquisitionFileNotChanged() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + take.PropertyAcquisitionFileId = 10; + _helper.AddAndSaveChanges(take); + + var newTake = EntityHelper.CreateTake(9999); + take.PropertyAcquisitionFileId = 50; + + // Act + var result = repository.UpdateTake(newTake); + + // Assert + result.PropertyAcquisitionFileId.Should().Be(50); + } + + [Fact] + public void UpdateTake_KeyNotFoundException() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + _helper.AddAndSaveChanges(take); + + var newTake = EntityHelper.CreateTake(1); + + // Act + Action act = () => repository.UpdateTake(newTake); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void TryDeleteTake_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + _helper.AddAndSaveChanges(take); + + // Act + var result = repository.TryDeleteTake(9999); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void TryDeleteTake_NoTake() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.AcquisitionFileView); + var take = EntityHelper.CreateTake(9999); + _helper.AddAndSaveChanges(take); + + // Act + var result = repository.TryDeleteTake(1); + + // Assert + result.Should().BeFalse(); + } + + #endregion + } +} diff --git a/source/frontend/package-lock.json b/source/frontend/package-lock.json index 7d9f95cc61..ca61510290 100644 --- a/source/frontend/package-lock.json +++ b/source/frontend/package-lock.json @@ -53,10 +53,12 @@ "redux-logger": "3.0.6", "redux-thunk": "2.3.0", "retry-axios": "2.4.0", + "save-dev": "^0.0.1-security", "styled-components": "5.3.6", "supercluster": "7.1.3", "tiles-in-bbox": "1.0.2", "tiny-invariant": "1.1.0", + "wait-for-expect": "^3.0.2", "xstate": "4.38.0", "yup": "0.32.9" }, @@ -17345,6 +17347,11 @@ "node": ">=8.9.0" } }, + "node_modules/save-dev": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/save-dev/-/save-dev-0.0.1-security.tgz", + "integrity": "sha512-k6knZTDNK8PKKbIqnvxiOveJinuw2LcQjqDoaorZWP9M5AR2EPsnpDeSbeoZZ0pHr5ze1uoaKdK8NBGQrJ34Uw==" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -20167,6 +20174,11 @@ "node": ">=18" } }, + "node_modules/wait-for-expect": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.2.tgz", + "integrity": "sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==" + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", diff --git a/source/frontend/package.json b/source/frontend/package.json index f035c02292..d573dbac80 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -48,10 +48,12 @@ "redux-logger": "3.0.6", "redux-thunk": "2.3.0", "retry-axios": "2.4.0", + "save-dev": "^0.0.1-security", "styled-components": "5.3.6", "supercluster": "7.1.3", "tiles-in-bbox": "1.0.2", "tiny-invariant": "1.1.0", + "wait-for-expect": "^3.0.2", "xstate": "4.38.0", "yup": "0.32.9" }, diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/add/TakesAddContainer.test.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/add/TakesAddContainer.test.tsx new file mode 100644 index 0000000000..a2617fd6ae --- /dev/null +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/add/TakesAddContainer.test.tsx @@ -0,0 +1,109 @@ +import { FormikProps } from 'formik'; +import { createMemoryHistory } from 'history'; +import { createRef, forwardRef } from 'react'; + +import { mockLookups } from '@/mocks/lookups.mock'; +import { getMockApiPropertyFiles } from '@/mocks/properties.mock'; +import { getMockApiTakes } from '@/mocks/takes.mock'; +import { lookupCodesSlice } from '@/store/slices/lookupCodes'; +import { act, render, RenderOptions, screen } from '@/utils/test-utils'; + +import { TakeModel } from '../models'; +import { useParams } from 'react-router-dom'; +import { useTakesRepository } from '../repositories/useTakesRepository'; +import TakesAddContainer, { ITakesDetailContainerProps } from './TakesAddContainer'; +import { ITakesFormProps, emptyTake } from '../update/TakeForm'; + +const history = createMemoryHistory(); +const storeState = { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, +}; + +const mockGetApi = { + error: undefined, + response: undefined, + execute: vi.fn(), + loading: false, + status: 200, +}; + +const mockAddApi = { + error: undefined, + response: undefined, + execute: vi.fn(), + loading: false, + status: 200, +}; +vi.mock('react-router-dom'); +vi.mocked(useParams).mockReturnValue({ takeId: '1' }); + +vi.mock('../repositories/useTakesRepository'); +vi.mocked(useTakesRepository).mockImplementation(() => ({ + getTakesByFileId: mockGetApi, + getTakesByPropertyId: mockGetApi, + getTakesCountByPropertyId: mockGetApi, + getTakeById: mockGetApi, + updateTakeByAcquisitionPropertyId: mockAddApi, + addTakeByAcquisitionPropertyId: mockAddApi, + deleteTakeByAcquisitionPropertyId: mockAddApi, +})); + +describe('TakeAddContainer component', () => { + // render component under test + + let viewProps: ITakesFormProps; + const View = forwardRef, ITakesFormProps>((props, ref) => { + viewProps = props; + return <>; + }); + + const onSuccess = vi.fn(); + + const setup = ( + renderOptions: RenderOptions & { props?: Partial }, + ) => { + const utils = render( + >()} + />, + { + ...renderOptions, + store: storeState, + history, + }, + ); + + return { + ...utils, + }; + }; + + afterEach(() => { + vi.clearAllMocks(); + }); + + beforeEach(() => { + history.push('takes/1'); + }); + + it('calls onSuccess when onSubmit method is called', async () => { + setup({}); + const formikHelpers = { setSubmitting: vi.fn() }; + await act(async () => + viewProps.onSubmit(new TakeModel(getMockApiTakes()[0]), formikHelpers as any), + ); + + expect(mockAddApi.execute).toHaveBeenCalled(); + expect(onSuccess).toHaveBeenCalled(); + }); + + it('returns an empty take', async () => { + setup({}); + + expect(viewProps.take).toStrictEqual(new TakeModel(emptyTake)); + }); +}); diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/add/TakesAddContainer.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/add/TakesAddContainer.tsx new file mode 100644 index 0000000000..df212be424 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/add/TakesAddContainer.tsx @@ -0,0 +1,43 @@ +import { FormikProps } from 'formik'; +import * as React from 'react'; + +import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; + +import { TakeModel } from '../models'; +import { useTakesRepository } from '../repositories/useTakesRepository'; +import { emptyTake, ITakesFormProps } from '../update/TakeForm'; + +export interface ITakesDetailContainerProps { + fileProperty: ApiGen_Concepts_FileProperty; + View: React.ForwardRefExoticComponent>>; + onSuccess: () => void; +} + +export const TakesAddContainer = React.forwardRef, ITakesDetailContainerProps>( + ({ fileProperty, View, onSuccess }, ref) => { + const { + addTakeByAcquisitionPropertyId: { execute: addTakesByPropertyFile, loading: addTakesLoading }, + } = useTakesRepository(); + + return ( + { + formikHelpers.setSubmitting(true); + try { + const take = values.toApi(); + take.propertyAcquisitionFileId = fileProperty.id; + await addTakesByPropertyFile(fileProperty.id, take); + onSuccess(); + } finally { + formikHelpers.setSubmitting(false); + } + }} + loading={addTakesLoading} + take={new TakeModel(emptyTake)} + ref={ref} + /> + ); + }, +); + +export default TakesAddContainer; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailContainer.test.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailContainer.test.tsx index 6e324b5e15..cf63fbf4a1 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailContainer.test.tsx @@ -9,7 +9,7 @@ import { lookupCodesSlice } from '@/store/slices/lookupCodes'; import { render, RenderOptions, waitFor } from '@/utils/test-utils'; import { useTakesRepository } from '../repositories/useTakesRepository'; -import { ITakesDetailContainerProps } from '../update/TakesUpdateContainer'; +import { ITakesDetailContainerProps } from '../update/TakeUpdateContainer'; import TakesDetailContainer from './TakesDetailContainer'; import { ITakesDetailViewProps } from './TakesDetailView'; import { IResponseWrapper } from '@/hooks/util/useApiRequestWrapper'; @@ -27,6 +27,7 @@ const mockGetApi = { response: undefined, execute: vi.fn(), loading: false, + status: 200, }; const mockCountsApi = { @@ -34,9 +35,27 @@ const mockCountsApi = { response: undefined, execute: vi.fn(), loading: false, + status: 200, +}; + +const mockUpdateApi = { + error: undefined, + response: undefined, + execute: vi.fn(), + loading: false, + status: 201, }; vi.mock('../repositories/useTakesRepository'); +vi.mocked(useTakesRepository).mockImplementation(() => ({ + getTakesByFileId: mockGetApi, + getTakesByPropertyId: mockGetApi, + getTakesCountByPropertyId: mockCountsApi, + getTakeById: mockGetApi, + updateTakeByAcquisitionPropertyId: mockUpdateApi, + addTakeByAcquisitionPropertyId: mockUpdateApi, + deleteTakeByAcquisitionPropertyId: mockUpdateApi, +})); describe('TakesDetailContainer component', () => { // render component under test @@ -47,8 +66,6 @@ describe('TakesDetailContainer component', () => { return <>; }); - const onEdit = vi.fn(); - const setup = ( renderOptions: RenderOptions & { props?: Partial }, ) => { @@ -57,7 +74,6 @@ describe('TakesDetailContainer component', () => { {...renderOptions.props} fileProperty={renderOptions.props?.fileProperty ?? getMockApiPropertyFiles()[0]} View={View} - onEdit={onEdit} />, { ...renderOptions, @@ -71,22 +87,15 @@ describe('TakesDetailContainer component', () => { }; }; - beforeEach(() => { - vi.mocked(useTakesRepository).mockReturnValue({ - getTakesByPropertyId: mockGetApi as unknown as IResponseWrapper< - (fileId: number, propertyId: number) => Promise> - >, - getTakesCountByPropertyId: mockCountsApi as unknown as IResponseWrapper< - (propertyId: number) => Promise> - >, - } as unknown as ReturnType); - }); - afterEach(() => { vi.clearAllMocks(); }); it('renders as expected', () => { + mockGetApi.response = [ + { ...getMockApiTakes(), id: 1 } as unknown as ApiGen_Concepts_Take, + { ...getMockApiTakes(), id: 2 } as unknown as ApiGen_Concepts_Take, + ]; const { asFragment } = setup({}); expect(asFragment()).toMatchSnapshot(); }); @@ -101,19 +110,10 @@ describe('TakesDetailContainer component', () => { }); it('returns the takes sorted by the id', async () => { - vi.mocked(useTakesRepository).mockReturnValue({ - getTakesByPropertyId: { - ...mockGetApi, - response: [ - { ...getMockApiTakes(), id: 1 } as unknown as ApiGen_Concepts_Take, - { ...getMockApiTakes(), id: 2 } as unknown as ApiGen_Concepts_Take, - ], - status: 200, - }, - getTakesCountByPropertyId: mockCountsApi as unknown as IResponseWrapper< - (propertyId: number) => Promise> - >, - } as unknown as ReturnType); + mockGetApi.response = [ + { ...getMockApiTakes(), id: 1 } as unknown as ApiGen_Concepts_Take, + { ...getMockApiTakes(), id: 2 } as unknown as ApiGen_Concepts_Take, + ]; setup({}); await waitFor(() => { @@ -122,7 +122,7 @@ describe('TakesDetailContainer component', () => { }); it('returns empty takes array by default', async () => { - mockGetApi.execute.mockResolvedValue(undefined); + mockGetApi.response = []; setup({}); await waitFor(() => { diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailContainer.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailContainer.tsx index 6f2d288b35..3167c65a18 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailContainer.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailContainer.tsx @@ -1,14 +1,21 @@ +import { AxiosError } from 'axios'; import orderBy from 'lodash/orderBy'; import { useEffect } from 'react'; +import { useCallback } from 'react'; +import { useHistory, useRouteMatch } from 'react-router-dom'; +import useApiUserOverride from '@/hooks/useApiUserOverride'; +import { useModalContext } from '@/hooks/useModalContext'; +import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; +import { UserOverrideCode } from '@/models/api/UserOverrideCode'; +import { stripTrailingSlash } from '@/utils/utils'; import { useTakesRepository } from '../repositories/useTakesRepository'; import { ITakesDetailViewProps } from './TakesDetailView'; interface ITakesDetailContainerProps { fileProperty: ApiGen_Concepts_FileProperty; - onEdit: (edit: boolean) => void; View: React.FunctionComponent>; } @@ -19,9 +26,10 @@ export interface IAreaWarning { const TakesDetailContainer: React.FunctionComponent = ({ fileProperty, - onEdit, View, }) => { + const history = useHistory(); + const { url } = useRouteMatch(); const fileId = fileProperty.fileId; const propertyId = fileProperty.property?.id; @@ -36,20 +44,51 @@ const TakesDetailContainer: React.FunctionComponent response: takesCount, execute: executeTakesCount, }, + deleteTakeByAcquisitionPropertyId: { loading: deleteTakesLoading, execute: executeTakeDelete }, } = useTakesRepository(); - useEffect(() => { + const refresh = useCallback(() => { fileId && propertyId && executeTakesByFileProperty(fileId, propertyId); propertyId && executeTakesCount(propertyId); }, [executeTakesByFileProperty, executeTakesCount, fileId, propertyId]); + useEffect(() => { + refresh(); + }, [executeTakesByFileProperty, executeTakesCount, fileId, propertyId, refresh]); + + const withUserOverride = useApiUserOverride< + (userOverrideCodes: UserOverrideCode[]) => Promise + >('Failed to add delete take'); + + const { setModalContent, setDisplayModal } = useModalContext(); + return ( history.push(`${stripTrailingSlash(url)}/${takeId}?edit=true`)} takes={orderBy(takes, t => t.id, 'desc') ?? []} - loading={takesByFileLoading && takesCountLoading} + loading={takesByFileLoading || takesCountLoading || deleteTakesLoading} allTakesCount={takesCount ?? 0} fileProperty={fileProperty} + onDelete={async takeId => { + return await withUserOverride( + (userOverrideCodes: UserOverrideCode[]) => + executeTakeDelete(fileProperty.id, takeId, userOverrideCodes).then(response => { + refresh(); + return response; + }), + [], + (axiosError: AxiosError) => { + setModalContent({ + variant: 'error', + title: 'Error', + message: axiosError?.response?.data.error, + okButtonText: 'Close', + }); + setDisplayModal(true); + }, + ); + }} + onAdd={() => history.push(`${stripTrailingSlash(url)}?edit=true`)} /> ); }; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.test.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.test.tsx index eb373a5939..beb316ca17 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.test.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.test.tsx @@ -11,6 +11,9 @@ import { act, render, RenderOptions, screen, userEvent, within } from '@/utils/t import TakesDetailView, { ITakesDetailViewProps } from './TakesDetailView'; import { ApiGen_CodeTypes_AcquisitionStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_AcquisitionStatusTypes'; +import { TAKE_STATUS_TYPES } from '@/constants/API'; +import { ApiGen_CodeTypes_AcquisitionTakeStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_AcquisitionTakeStatusTypes'; +import Roles from '@/constants/roles'; const history = createMemoryHistory(); const storeState = { @@ -18,6 +21,8 @@ const storeState = { }; const onEdit = vi.fn(); +const onAdd = vi.fn(); +const onDelete = vi.fn(); describe('TakesDetailView component', () => { // render component under test @@ -30,6 +35,8 @@ describe('TakesDetailView component', () => { loading={renderOptions.props?.loading ?? false} fileProperty={renderOptions.props?.fileProperty ?? getMockApiPropertyFiles()[0]} onEdit={onEdit} + onAdd={onAdd} + onDelete={onDelete} />, { ...renderOptions, @@ -60,8 +67,11 @@ describe('TakesDetailView component', () => { }); it('clicking the edit button fires the edit event', async () => { - const { getByTitle } = setup({ props: { loading: true }, claims: [Claims.PROPERTY_EDIT] }); - const editButton = getByTitle('Edit takes'); + const { getByTitle } = setup({ + props: { loading: true, takes: getMockApiTakes() }, + claims: [Claims.PROPERTY_EDIT, Claims.ACQUISITION_EDIT], + }); + const editButton = getByTitle('Edit take'); await act(async () => userEvent.click(editButton)); expect(onEdit).toHaveBeenCalled(); }); @@ -79,15 +89,130 @@ describe('TakesDetailView component', () => { fileStatusTypeCode: toTypeCodeNullable(ApiGen_CodeTypes_AcquisitionStatusTypes.COMPLT), }, }, + takes: getMockApiTakes(), }, claims: [Claims.PROPERTY_EDIT], }); - const editButton = queryByTitle('Edit takes'); + const editButton = queryByTitle('Edit take'); expect(editButton).toBeNull(); const tooltip = getByTestId('tooltip-icon-1-summary-cannot-edit-tooltip'); expect(tooltip).toBeVisible(); }); + it('hides the edit button when the take has been completed', () => { + const { queryByTitle, getByTestId } = setup({ + props: { + loading: true, + takes: [ + { + ...getMockApiTakes()[0], + takeStatusTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE.toString(), + ), + }, + ], + }, + claims: [Claims.PROPERTY_EDIT, Claims.ACQUISITION_EDIT], + }); + const editButton = queryByTitle('Edit take'); + expect(editButton).toBeNull(); + const tooltip = getByTestId('tooltip-icon-1-summary-cannot-edit-tooltip'); + expect(tooltip).toBeVisible(); + }); + + it('does not hide the edit button when the user is an admin even if the take is complete', async () => { + const { getByTitle } = setup({ + props: { + loading: true, + takes: [ + { + ...getMockApiTakes()[0], + takeStatusTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE.toString(), + ), + }, + ], + }, + claims: [Claims.PROPERTY_EDIT], + roles: [Roles.SYSTEM_ADMINISTRATOR], + }); + const editButton = getByTitle('Edit take'); + await act(async () => userEvent.click(editButton)); + expect(onEdit).toHaveBeenCalled(); + }); + + it('clicking the delete button fires the edit event', async () => { + const { getByTitle } = setup({ + props: { loading: true, takes: getMockApiTakes() }, + claims: [Claims.PROPERTY_EDIT, Claims.ACQUISITION_EDIT], + }); + const removeButton = getByTitle('Remove take'); + await act(async () => userEvent.click(removeButton)); + const yesButton = screen.getByTestId('ok-modal-button'); + await act(async () => userEvent.click(yesButton)); + expect(onDelete).toHaveBeenCalled(); + }); + + it('hides the delete button when the file has been completed', () => { + const fileProperty = getMockApiPropertyFiles()[0]; + const file: ApiGen_Concepts_File = fileProperty!.file as ApiGen_Concepts_File; + const { queryByTitle, getByTestId } = setup({ + props: { + loading: true, + fileProperty: { + ...fileProperty, + file: { + ...file, + fileStatusTypeCode: toTypeCodeNullable(ApiGen_CodeTypes_AcquisitionStatusTypes.COMPLT), + }, + }, + takes: getMockApiTakes(), + }, + claims: [Claims.PROPERTY_EDIT], + }); + const removeButton = queryByTitle('Remove take'); + expect(removeButton).toBeNull(); + }); + + it('hides the delete button when the take has been completed', () => { + const { queryByTitle, getByTestId } = setup({ + props: { + loading: true, + takes: [ + { + ...getMockApiTakes()[0], + takeStatusTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE.toString(), + ), + }, + ], + }, + claims: [Claims.PROPERTY_EDIT], + }); + const removeButton = queryByTitle('Remove take'); + expect(removeButton).toBeNull(); + }); + + it('does not hide delete button when the take has been completed and user is an admin', () => { + const { queryByTitle, getByTestId } = setup({ + props: { + loading: true, + takes: [ + { + ...getMockApiTakes()[0], + takeStatusTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE.toString(), + ), + }, + ], + }, + claims: [Claims.PROPERTY_EDIT], + roles: [Roles.SYSTEM_ADMINISTRATOR], + }); + const removeButton = queryByTitle('Remove take'); + expect(removeButton).toBeVisible(); + }); + it('displays the number of takes in other files', () => { setup({ props: { diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.tsx index a901f90d9e..61c3354100 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.tsx @@ -1,20 +1,25 @@ +import { Col, Row } from 'react-bootstrap'; +import { FaPlus, FaTrash } from 'react-icons/fa'; import styled from 'styled-components'; +import { StyledRemoveLinkButton } from '@/components/common/buttons'; import YesNoButtons from '@/components/common/buttons/YesNoButtons'; import EditButton from '@/components/common/EditButton'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import { Section } from '@/components/common/Section/Section'; import { SectionField } from '@/components/common/Section/SectionField'; import { StyledEditWrapper, StyledSummarySection } from '@/components/common/Section/SectionStyles'; +import { SectionListHeader } from '@/components/common/SectionListHeader'; import { H2 } from '@/components/common/styles'; import TooltipIcon from '@/components/common/TooltipIcon'; import AreaContainer from '@/components/measurements/AreaContainer'; +import { Claims, Roles } from '@/constants'; import * as API from '@/constants/API'; -import { Claims } from '@/constants/claims'; import { isAcquisitionFile } from '@/features/mapSideBar/acquisition/add/models'; import StatusUpdateSolver from '@/features/mapSideBar/acquisition/tabs/fileDetails/detail/statusUpdateSolver'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; +import { getDeleteModalProps, useModalContext } from '@/hooks/useModalContext'; import { ApiGen_CodeTypes_AcquisitionTakeStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_AcquisitionTakeStatusTypes'; import { ApiGen_CodeTypes_LandActTypes } from '@/models/api/generated/ApiGen_CodeTypes_LandActTypes'; import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; @@ -28,7 +33,9 @@ export interface ITakesDetailViewProps { allTakesCount: number; loading: boolean; fileProperty: ApiGen_Concepts_FileProperty; - onEdit: (edit: boolean) => void; + onEdit: (takeId: number) => void; + onAdd: () => void; + onDelete: (takeId: number) => void; } export const TakesDetailView: React.FunctionComponent = ({ @@ -37,6 +44,8 @@ export const TakesDetailView: React.FunctionComponent = ( fileProperty, loading, onEdit, + onAdd, + onDelete, }) => { const cancelledTakes = takes?.filter( t => t.takeStatusTypeCode?.id === ApiGen_CodeTypes_AcquisitionTakeStatusTypes.CANCELLED, @@ -47,14 +56,21 @@ export const TakesDetailView: React.FunctionComponent = ( const takesNotInFile = allTakesCount - (takes?.length ?? 0); const { getCodeById } = useLookupCodeHelpers(); - const { hasClaim } = useKeycloakWrapper(); + const { hasClaim, hasRole } = useKeycloakWrapper(); + const { setModalContent, setDisplayModal } = useModalContext(); const file = fileProperty.file; const statusSolver = new StatusUpdateSolver(isAcquisitionFile(file) ? file : null); - const canEditDetails = () => { - if (statusSolver.canEditDetails()) { + const canEditTakes = (take: ApiGen_Concepts_Take) => { + if ( + (statusSolver.canEditTakes() && + take.takeStatusTypeCode.id !== + ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE.toString() && + hasClaim(Claims.ACQUISITION_EDIT)) || + hasRole(Roles.SYSTEM_ADMINISTRATOR) + ) { return true; } return false; @@ -63,22 +79,6 @@ export const TakesDetailView: React.FunctionComponent = ( return ( - - {onEdit !== undefined && hasClaim(Claims.PROPERTY_EDIT) && canEditDetails() ? ( - { - onEdit(true); - }} - /> - ) : null} - {!canEditDetails() && ( - - )} -

Takes for {getApiPropertyName(fileProperty.property).value}

@@ -99,182 +99,234 @@ export const TakesDetailView: React.FunctionComponent = (
- {[...nonCancelledTakes, ...cancelledTakes].map((take, index) => { - return ( -
-

Take {index + 1}

- - {prettyFormatUTCDate(take.appCreateTimestamp)} - - - {take.takeTypeCode?.id ? getCodeById(API.TAKE_TYPES, take.takeTypeCode.id) : ''} - - - {take.takeStatusTypeCode?.id - ? getCodeById(API.TAKE_STATUS_TYPES, take.takeStatusTypeCode.id) - : ''} - - {take.completionDt && ( - - {prettyFormatDate(take.completionDt)} +
+

+ } + addButtonText="Add Take" + onAdd={onAdd} + /> +

+ {[...nonCancelledTakes, ...cancelledTakes].map((take, index) => { + return ( +
+ Take {index + 1} + + + {onEdit !== undefined && canEditTakes(take) ? ( + onEdit(take.id)} /> + ) : null} + {!canEditTakes(take) && ( + + )} + + {canEditTakes(take) && ( + { + setModalContent({ + ...getDeleteModalProps(), + handleOk: () => { + onDelete(take.id); + setDisplayModal(false); + }, + }); + setDisplayModal(true); + }} + > + + + )} + + + } + > + + {prettyFormatUTCDate(take.appCreateTimestamp)} + + + {take.takeTypeCode?.id ? getCodeById(API.TAKE_TYPES, take.takeTypeCode.id) : ''} - )} - - {take.takeSiteContamTypeCode?.id - ? getCodeById(API.TAKE_SITE_CONTAM_TYPES, take.takeSiteContamTypeCode.id) - : ''} - - - {take.description} - - - - - + + {take.takeStatusTypeCode?.id + ? getCodeById(API.TAKE_STATUS_TYPES, take.takeStatusTypeCode.id) + : ''} + + {take.completionDt && ( + + {prettyFormatDate(take.completionDt)} - {take.isNewHighwayDedication && ( - - + )} + + {take.takeSiteContamTypeCode?.id + ? getCodeById(API.TAKE_SITE_CONTAM_TYPES, take.takeSiteContamTypeCode.id) + : ''} + + + {take.description} + + + + + - )} - - - - - - - - - {take.isNewInterestInSrw && ( - <> + {take.isNewHighwayDedication && ( - + + )} + + + + + + + + + {take.isNewInterestInSrw && ( + <> + + + - - {prettyFormatDate(take.srwEndDt ?? undefined)} - - - )} - - - - - - {take.isNewLandAct && ( - <> - - {take.landActTypeCode - ? take.landActTypeCode.id + ' ' + take.landActTypeCode.description - : ''} - + + {prettyFormatDate(take.srwEndDt ?? undefined)} + + + )} + + + + + + {take.isNewLandAct && ( + <> + + {take.landActTypeCode + ? take.landActTypeCode.id + ' ' + take.landActTypeCode.description + : ''} + - - - + + + - {![ - ApiGen_CodeTypes_LandActTypes.TRANSFER_OF_ADMIN_AND_CONTROL.toString(), - ApiGen_CodeTypes_LandActTypes.CROWN_GRANT.toString(), - ].includes(take.landActTypeCode.id) && ( - - {prettyFormatDate(take.landActEndDt ?? undefined)} + {![ + ApiGen_CodeTypes_LandActTypes.TRANSFER_OF_ADMIN_AND_CONTROL.toString(), + ApiGen_CodeTypes_LandActTypes.CROWN_GRANT.toString(), + ].includes(take.landActTypeCode.id) && ( + + {prettyFormatDate(take.landActEndDt ?? undefined)} + + )} + + )} + + + + + + {take.isNewLicenseToConstruct && ( + <> + + - )} - - )} - - - - - - {take.isNewLicenseToConstruct && ( - <> - - - - - {prettyFormatDate(take.ltcEndDt ?? undefined)} - - - )} - - - - - - {take.isLeasePayable && ( - <> - - - + + {prettyFormatDate(take.ltcEndDt ?? undefined)} + + + )} + + + + + + {take.isLeasePayable && ( + <> + + + - - {prettyFormatDate(take.leasePayableEndDt ?? undefined)} - - - )} - - - - - - - - {take.isThereSurplus && ( - - + + {prettyFormatDate(take.leasePayableEndDt ?? undefined)} + + + )} + + + + + + - )} - - -
- ); - })} + {take.isThereSurplus && ( + + + + )} + + +
+ ); + })} +
); }; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/__snapshots__/TakesDetailView.test.tsx.snap b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/__snapshots__/TakesDetailView.test.tsx.snap index 0a2e33dcfe..09621d57ab 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/__snapshots__/TakesDetailView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/__snapshots__/TakesDetailView.test.tsx.snap @@ -6,7 +6,7 @@ exports[`TakesDetailView component > renders as expected 1`] = ` class="Toastify" />
- .c3 { + .c2 { font-family: 'BCSans-Bold'; font-size: 2.6rem; border-bottom: solid 0.2rem; @@ -20,10 +20,6 @@ exports[`TakesDetailView component > renders as expected 1`] = ` } .c1 { - text-align: right; -} - -.c2 { margin: 1.5rem; padding: 1.5rem; background-color: white; @@ -31,18 +27,34 @@ exports[`TakesDetailView component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c6.required::before { +.c5.required::before { content: '*'; position: absolute; top: 0.75rem; left: 0rem; } -.c5 { +.c4 { font-weight: bold; } -.c4 { +.c6 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-align-items: end; + -webkit-box-align: end; + -ms-flex-align: end; + align-items: end; + min-height: 4.5rem; +} + +.c6 .btn { + margin: 0; +} + +.c3 { border-radius: 0.5rem; padding: 1rem; } @@ -51,21 +63,18 @@ exports[`TakesDetailView component > renders as expected 1`] = ` class="c0" >
-

Takes for 007-723-385

renders as expected 1`] = ` class="pr-0 text-left col-8" >
0
@@ -116,7 +125,7 @@ exports[`TakesDetailView component > renders as expected 1`] = ` class="pr-0 text-left col-8" >
0 @@ -155,6 +164,30 @@ exports[`TakesDetailView component > renders as expected 1`] = `
+
+
+

+
+
+ Takes +
+
+
+

+
+
`; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/models.test.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/models.test.tsx similarity index 98% rename from source/frontend/src/features/mapSideBar/property/tabs/takes/update/models.test.tsx rename to source/frontend/src/features/mapSideBar/property/tabs/takes/models.test.tsx index 979f2f106e..bf79c2ac72 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/models.test.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/models.test.tsx @@ -2,7 +2,7 @@ import { ApiGen_Concepts_Take } from '@/models/api/generated/ApiGen_Concepts_Tak import { toTypeCode } from '@/utils/formUtils'; import { TakeModel } from './models'; -import { emptyTake } from './TakesUpdateForm'; +import { emptyTake } from './update/TakeForm'; describe('take model tests', () => { it("converts all false it values to 'false'", () => { diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/models.ts b/source/frontend/src/features/mapSideBar/property/tabs/takes/models.ts similarity index 52% rename from source/frontend/src/features/mapSideBar/property/tabs/takes/update/models.ts rename to source/frontend/src/features/mapSideBar/property/tabs/takes/models.ts index 08c852cc5e..ce20c9c3eb 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/models.ts +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/models.ts @@ -8,46 +8,42 @@ import { getEmptyBaseAudit } from '@/models/defaultInitializers'; import { convertArea } from '@/utils/convertUtils'; import { fromTypeCodeNullable, stringToNull, toTypeCodeNullable } from '@/utils/formUtils'; -import { ApiGen_CodeTypes_AcquisitionTakeStatusTypes } from './../../../../../../models/api/generated/ApiGen_CodeTypes_AcquisitionTakeStatusTypes'; +import { ApiGen_CodeTypes_AcquisitionTakeStatusTypes } from '../../../../../models/api/generated/ApiGen_CodeTypes_AcquisitionTakeStatusTypes'; /* eslint-disable no-template-curly-in-string */ export const TakesYupSchema = Yup.object().shape({ - takes: Yup.array().of( - Yup.object().shape({ - description: Yup.string().max(4000, 'Description must be at most ${max} characters'), - takeTypeCode: Yup.string().required('Take type is required').nullable(), - takeStatusTypeCode: Yup.string().required('Take status type is required.'), - isThereSurplus: Yup.bool().required('Surplus flag required'), - isNewHighwayDedication: Yup.bool().required('New highway dedication flag required'), - isNewLandAct: Yup.bool().required('Section 16 flag required'), - isNewInterestInSrw: Yup.bool().required('Statutory right of way (SRW) flag required'), - isNewLicenseToConstruct: Yup.bool().required('License to construct flag required'), - ltcEndDt: Yup.string().when('isNewLicenseToConstruct', { - is: (isNewLicenseToConstruct: boolean) => isNewLicenseToConstruct, - then: Yup.string().required('End Date is required'), - }), - landActEndDt: Yup.string().when(['isNewLandAct', 'landActTypeCode'], { - is: (isNewLandAct: boolean, landActTypeCode: string) => - isNewLandAct && - ![ - ApiGen_CodeTypes_LandActTypes.TRANSFER_OF_ADMIN_AND_CONTROL.toString(), - ApiGen_CodeTypes_LandActTypes.CROWN_GRANT.toString(), - ].includes(landActTypeCode), - then: Yup.string().required('End Date is required'), - }), - landActTypeCode: Yup.string().when('isNewLandAct', { - is: (isNewLandAct: boolean) => isNewLandAct, - then: Yup.string().required('Land Act is required'), - }), - completionDt: Yup.string() - .nullable() - .when('takeStatusTypeCode', { - is: (takeStatusTypeCode: string) => - takeStatusTypeCode === ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE, - then: Yup.string().nullable().required('A completed take must have a completion date.'), - }), + description: Yup.string().max(4000, 'Description must be at most ${max} characters'), + takeTypeCode: Yup.string().required('Take type is required').nullable(), + takeStatusTypeCode: Yup.string().required('Take status type is required.'), + isThereSurplus: Yup.bool().required('Surplus flag required'), + isNewHighwayDedication: Yup.bool().required('New highway dedication flag required'), + isNewLandAct: Yup.bool().required('Section 16 flag required'), + isNewInterestInSrw: Yup.bool().required('Statutory right of way (SRW) flag required'), + isNewLicenseToConstruct: Yup.bool().required('License to construct flag required'), + ltcEndDt: Yup.string().when('isNewLicenseToConstruct', { + is: (isNewLicenseToConstruct: boolean) => isNewLicenseToConstruct, + then: Yup.string().required('End Date is required'), + }), + landActEndDt: Yup.string().when(['isNewLandAct', 'landActTypeCode'], { + is: (isNewLandAct: boolean, landActTypeCode: string) => + isNewLandAct && + ![ + ApiGen_CodeTypes_LandActTypes.TRANSFER_OF_ADMIN_AND_CONTROL.toString(), + ApiGen_CodeTypes_LandActTypes.CROWN_GRANT.toString(), + ].includes(landActTypeCode), + then: Yup.string().required('End Date is required'), + }), + landActTypeCode: Yup.string().when('isNewLandAct', { + is: (isNewLandAct: boolean) => isNewLandAct, + then: Yup.string().required('Land Act is required'), + }), + completionDt: Yup.string() + .nullable() + .when('takeStatusTypeCode', { + is: (takeStatusTypeCode: string) => + takeStatusTypeCode === ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE, + then: Yup.string().nullable().required('A completed take must have a completion date.'), }), - ), }); export class TakeModel { @@ -86,50 +82,50 @@ export class TakeModel { rowVersion?: number; appCreateTimestamp: UtcIsoDateTime | null; - constructor(base: ApiGen_Concepts_Take) { - this.id = base.id; - this.rowVersion = base.rowVersion ?? undefined; - this.description = base.description ?? ''; - this.isThereSurplus = base.isThereSurplus ? 'true' : 'false'; - this.isNewHighwayDedication = base.isNewHighwayDedication ? 'true' : 'false'; - this.isNewLandAct = base.isNewLandAct ? 'true' : 'false'; - this.isNewLicenseToConstruct = base.isNewLicenseToConstruct ? 'true' : 'false'; - this.isNewInterestInSrw = base.isNewInterestInSrw ? 'true' : 'false'; - this.isLeasePayable = base.isLeasePayable ? 'true' : 'false'; - this.licenseToConstructArea = base.licenseToConstructArea ?? 0; + constructor(base?: ApiGen_Concepts_Take) { + this.id = base?.id; + this.rowVersion = base?.rowVersion ?? undefined; + this.description = base?.description ?? ''; + this.isThereSurplus = base?.isThereSurplus ? 'true' : 'false'; + this.isNewHighwayDedication = base?.isNewHighwayDedication ? 'true' : 'false'; + this.isNewLandAct = base?.isNewLandAct ? 'true' : 'false'; + this.isNewLicenseToConstruct = base?.isNewLicenseToConstruct ? 'true' : 'false'; + this.isNewInterestInSrw = base?.isNewInterestInSrw ? 'true' : 'false'; + this.isLeasePayable = base?.isLeasePayable ? 'true' : 'false'; + this.licenseToConstructArea = base?.licenseToConstructArea ?? 0; this.licenseToConstructAreaUnitTypeCode = - fromTypeCodeNullable(base.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); - this.landActArea = base.landActArea ?? 0; + fromTypeCodeNullable(base?.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); + this.landActArea = base?.landActArea ?? 0; this.landActAreaUnitTypeCode = - fromTypeCodeNullable(base.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); - this.surplusArea = base.surplusArea ?? 0; + fromTypeCodeNullable(base?.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); + this.surplusArea = base?.surplusArea ?? 0; this.surplusAreaUnitTypeCode = - fromTypeCodeNullable(base.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); - this.statutoryRightOfWayArea = base.statutoryRightOfWayArea ?? 0; + fromTypeCodeNullable(base?.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); + this.statutoryRightOfWayArea = base?.statutoryRightOfWayArea ?? 0; this.statutoryRightOfWayAreaUnitTypeCode = - fromTypeCodeNullable(base.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); + fromTypeCodeNullable(base?.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); this.leasePayableAreaUnitTypeCode = - fromTypeCodeNullable(base.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); - this.takeTypeCode = fromTypeCodeNullable(base.takeTypeCode); - this.takeStatusTypeCode = fromTypeCodeNullable(base.takeStatusTypeCode); - this.takeSiteContamTypeCode = base.takeSiteContamTypeCode - ? fromTypeCodeNullable(base.takeSiteContamTypeCode) + fromTypeCodeNullable(base?.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); + this.takeTypeCode = fromTypeCodeNullable(base?.takeTypeCode); + this.takeStatusTypeCode = fromTypeCodeNullable(base?.takeStatusTypeCode); + this.takeSiteContamTypeCode = base?.takeSiteContamTypeCode + ? fromTypeCodeNullable(base?.takeSiteContamTypeCode) : 'UNK'; - this.propertyAcquisitionFileId = base.propertyAcquisitionFileId; - this.landActEndDt = base.landActEndDt ?? ''; - this.ltcEndDt = base.ltcEndDt ?? ''; - this.srwEndDt = base.srwEndDt ?? ''; - this.leasePayableEndDt = base.leasePayableEndDt ?? ''; - this.leasePayableArea = base.leasePayableArea ?? 0; - this.landActDescription = base.landActTypeCode?.description ?? ''; - this.landActTypeCode = base.landActTypeCode?.id ?? ''; + this.propertyAcquisitionFileId = base?.propertyAcquisitionFileId; + this.landActEndDt = base?.landActEndDt ?? ''; + this.ltcEndDt = base?.ltcEndDt ?? ''; + this.srwEndDt = base?.srwEndDt ?? ''; + this.leasePayableEndDt = base?.leasePayableEndDt ?? ''; + this.leasePayableArea = base?.leasePayableArea ?? 0; + this.landActDescription = base?.landActTypeCode?.description ?? ''; + this.landActTypeCode = base?.landActTypeCode?.id ?? ''; - this.isAcquiredForInventory = base.isAcquiredForInventory ? 'true' : 'false'; - this.newHighwayDedicationArea = base.newHighwayDedicationArea ?? 0; + this.isAcquiredForInventory = base?.isAcquiredForInventory ? 'true' : 'false'; + this.newHighwayDedicationArea = base?.newHighwayDedicationArea ?? 0; this.newHighwayDedicationAreaUnitTypeCode = - fromTypeCodeNullable(base.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); - this.completionDt = base.completionDt; - this.appCreateTimestamp = base.appCreateTimestamp ?? null; + fromTypeCodeNullable(base?.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); + this.completionDt = base?.completionDt; + this.appCreateTimestamp = base?.appCreateTimestamp ?? null; } toApi(): ApiGen_Concepts_Take { diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/repositories/useTakesRepository.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/repositories/useTakesRepository.tsx index b953412338..763a7c3140 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/repositories/useTakesRepository.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/repositories/useTakesRepository.tsx @@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react'; import { useApiTakes } from '@/hooks/pims-api/useApiTakes'; import { useApiRequestWrapper } from '@/hooks/util/useApiRequestWrapper'; import { ApiGen_Concepts_Take } from '@/models/api/generated/ApiGen_Concepts_Take'; +import { UserOverrideCode } from '@/models/api/UserOverrideCode'; import { useAxiosErrorHandler, useAxiosSuccessHandler } from '@/utils'; /** @@ -13,10 +14,29 @@ export const useTakesRepository = () => { const { getTakesByAcqFileId, getTakesCountByPropertyId, - updateTakesCountByPropertyId, + addTakeByFilePropertyId, + getTakeById, + updateTakeByFilePropertyId, + deleteTakeByFilePropertyId, getTakesByPropertyId, } = useApiTakes(); + const getTakeByIdApi = useApiRequestWrapper< + ( + acquisitionFilePropertyId: number, + takeId: number, + ) => Promise> + >({ + requestFunction: useCallback( + async (acquisitionFilePropertyId: number, takeId: number) => + await getTakeById(acquisitionFilePropertyId, takeId), + [getTakeById], + ), + requestName: 'GetTakeByIdApi', + onSuccess: useAxiosSuccessHandler(), + onError: useAxiosErrorHandler(), + }); + const getTakesByFileIdApi = useApiRequestWrapper< (fileId: number) => Promise> >({ @@ -53,16 +73,33 @@ export const useTakesRepository = () => { onError: useAxiosErrorHandler(), }); - const updateTakesByAcquisitionPropertyIdApi = useApiRequestWrapper< + const addTakeByAcquisitionPropertyIdApi = useApiRequestWrapper< ( acquisitionFilePropertyId: number, - takes: ApiGen_Concepts_Take[], - ) => Promise> + take: ApiGen_Concepts_Take, + ) => Promise> + >({ + requestFunction: useCallback( + async (acquisitionFilePropertyId: number, take: ApiGen_Concepts_Take) => + await addTakeByFilePropertyId(acquisitionFilePropertyId, take), + [addTakeByFilePropertyId], + ), + requestName: 'AddTakeByAcquisitionPropertyIdApi', + onSuccess: useAxiosSuccessHandler(), + onError: useAxiosErrorHandler(), + throwError: true, + }); + + const updateTakeByAcquisitionPropertyIdApi = useApiRequestWrapper< + ( + acquisitionFilePropertyId: number, + take: ApiGen_Concepts_Take, + ) => Promise> >({ requestFunction: useCallback( - async (acquisitionFilePropertyId: number, takes: ApiGen_Concepts_Take[]) => - await updateTakesCountByPropertyId(acquisitionFilePropertyId, takes), - [updateTakesCountByPropertyId], + async (acquisitionFilePropertyId: number, take: ApiGen_Concepts_Take) => + await updateTakeByFilePropertyId(acquisitionFilePropertyId, take), + [updateTakeByFilePropertyId], ), requestName: 'UpdateTakesByAcquisitionPropertyId', onSuccess: useAxiosSuccessHandler(), @@ -70,18 +107,44 @@ export const useTakesRepository = () => { throwError: true, }); + const deleteTakeByAcquisitionPropertyIdApi = useApiRequestWrapper< + ( + acquisitionFilePropertyId: number, + takeId: number, + userOverrideCodes: UserOverrideCode[], + ) => Promise> + >({ + requestFunction: useCallback( + async ( + acquisitionFilePropertyId: number, + takeId: number, + userOverrideCodes: UserOverrideCode[], + ) => await deleteTakeByFilePropertyId(acquisitionFilePropertyId, takeId, userOverrideCodes), + [deleteTakeByFilePropertyId], + ), + requestName: 'deleteTakeByAcquisitionPropertyIdApi', + onSuccess: useAxiosSuccessHandler(), + throwError: true, + }); + return useMemo( () => ({ + getTakeById: getTakeByIdApi, getTakesByFileId: getTakesByFileIdApi, getTakesByPropertyId: getTakesByPropertyApi, getTakesCountByPropertyId: getTakesCountByPropertyIdApi, - updateTakesByAcquisitionPropertyId: updateTakesByAcquisitionPropertyIdApi, + addTakeByAcquisitionPropertyId: addTakeByAcquisitionPropertyIdApi, + updateTakeByAcquisitionPropertyId: updateTakeByAcquisitionPropertyIdApi, + deleteTakeByAcquisitionPropertyId: deleteTakeByAcquisitionPropertyIdApi, }), [ + getTakeByIdApi, getTakesByFileIdApi, getTakesByPropertyApi, getTakesCountByPropertyIdApi, - updateTakesByAcquisitionPropertyIdApi, + addTakeByAcquisitionPropertyIdApi, + updateTakeByAcquisitionPropertyIdApi, + deleteTakeByAcquisitionPropertyIdApi, ], ); }; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeForm.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeForm.tsx new file mode 100644 index 0000000000..c0a6fa6229 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeForm.tsx @@ -0,0 +1,70 @@ +import { Formik, FormikHelpers, FormikProps } from 'formik'; +import * as React from 'react'; + +import LoadingBackdrop from '@/components/common/LoadingBackdrop'; +import { StyledSummarySection } from '@/components/common/Section/SectionStyles'; +import { AreaUnitTypes } from '@/constants/areaUnitTypes'; +import { ApiGen_Concepts_Take } from '@/models/api/generated/ApiGen_Concepts_Take'; +import { getEmptyBaseAudit } from '@/models/defaultInitializers'; +import { toTypeCode } from '@/utils/formUtils'; + +import { TakeModel, TakesYupSchema } from '../models'; +import TakeSubForm from './TakeSubForm'; + +export interface ITakesFormProps { + take: TakeModel; + loading: boolean; + onSubmit: (model: TakeModel, formikHelpers: FormikHelpers) => Promise; +} + +export const TakeForm = React.forwardRef, ITakesFormProps>( + ({ take, loading, onSubmit }, ref) => { + return ( + + + + onSubmit={onSubmit} + initialValues={take} + innerRef={ref} + validationSchema={TakesYupSchema} + enableReinitialize + > + {({ values }) => } + + + ); + }, +); + +export const emptyTake: ApiGen_Concepts_Take = { + id: 0, + description: '', + newHighwayDedicationArea: null, + areaUnitTypeCode: toTypeCode(AreaUnitTypes.SquareMeters.toString()), + isAcquiredForInventory: null, + isThereSurplus: null, + isNewLicenseToConstruct: null, + isNewHighwayDedication: null, + isNewLandAct: null, + isNewInterestInSrw: null, + isLeasePayable: null, + licenseToConstructArea: null, + ltcEndDt: null, + landActArea: null, + landActEndDt: null, + propertyAcquisitionFile: null, + propertyAcquisitionFileId: 0, + statutoryRightOfWayArea: null, + srwEndDt: null, + surplusArea: null, + leasePayableArea: null, + leasePayableEndDt: null, + takeSiteContamTypeCode: null, + takeTypeCode: null, + takeStatusTypeCode: toTypeCode('INPROGRESS'), + landActTypeCode: null, + completionDt: null, + ...getEmptyBaseAudit(), +}; + +export default TakeForm; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeSubForm.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeSubForm.tsx index 622b605a1d..2f3865d5c8 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeSubForm.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeSubForm.tsx @@ -1,8 +1,6 @@ import { getIn, useFormikContext } from 'formik'; import { useEffect } from 'react'; -import { FaTrash } from 'react-icons/fa'; -import { StyledRemoveLinkButton } from '@/components/common/buttons'; import { FastDatePicker, Select, TextArea } from '@/components/common/form'; import { RadioGroup, yesNoRadioGroupValues } from '@/components/common/form/RadioGroup'; import { Section } from '@/components/common/Section/Section'; @@ -12,27 +10,20 @@ import { Roles } from '@/constants'; import * as API from '@/constants/API'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; -import { getDeleteModalProps, useModalContext } from '@/hooks/useModalContext'; +import { useModalContext } from '@/hooks/useModalContext'; import { ApiGen_CodeTypes_AcquisitionTakeStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_AcquisitionTakeStatusTypes'; import { ApiGen_CodeTypes_LandActTypes } from '@/models/api/generated/ApiGen_CodeTypes_LandActTypes'; -import { withNameSpace } from '@/utils/formUtils'; +import { TakeModel } from '../models'; import { StyledBorderSection, StyledNoTabSection } from '../styles'; interface ITakeSubFormProps { - takeIndex: number; - nameSpace: string; - onRemove: (index: number) => void; + take: TakeModel; } -const TakeSubForm: React.FunctionComponent = ({ - takeIndex, - nameSpace, - onRemove, -}) => { +const TakeSubForm: React.FunctionComponent = ({ take }) => { const formikProps = useFormikContext(); const { values, setFieldValue, handleChange } = formikProps; - const currentTake = getIn(values, withNameSpace(nameSpace)); const { getOptionsByType } = useLookupCodeHelpers(); const { setModalContent, setDisplayModal } = useModalContext(); const { hasRole } = useKeycloakWrapper(); @@ -45,25 +36,14 @@ const TakeSubForm: React.FunctionComponent = ({ label: landAct.value + ' ' + landAct.label, })); - const isThereSurplus = getIn(values, withNameSpace(nameSpace, 'isThereSurplus')); - const isNewHighwayDedication = getIn(values, withNameSpace(nameSpace, 'isNewHighwayDedication')); - const isNewInterestInSrw = getIn(values, withNameSpace(nameSpace, 'isNewInterestInSrw')); - const isNewLandAct = getIn(values, withNameSpace(nameSpace, 'isNewLandAct')); - const isNewLicenseToConstruct = getIn( - values, - withNameSpace(nameSpace, 'isNewLicenseToConstruct'), - ); - const isLeasePayable = getIn(values, withNameSpace(nameSpace, 'isLeasePayable')); - const takeStatusTypeCode = getIn(values, withNameSpace(nameSpace, 'takeStatusTypeCode')); - useEffect(() => { if ( - currentTake.completionDt && - currentTake.takeStatusTypeCode !== ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE + take.completionDt && + take.takeStatusTypeCode !== ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE ) { - setFieldValue(withNameSpace(nameSpace, 'completionDt'), ''); + setFieldValue('completionDt', ''); } - }, [currentTake.completionDt, currentTake.takeStatusTypeCode, nameSpace, setFieldValue]); + }, [take.completionDt, take.takeStatusTypeCode, setFieldValue]); const getModalWarning = (onOk: () => void, isLeasePayable = false) => { return (e: React.ChangeEvent) => { @@ -101,39 +81,20 @@ const TakeSubForm: React.FunctionComponent = ({ }; const canEditTake = - currentTake?.id === 0 || - takeStatusTypeCode !== ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE || + take?.id === 0 || + take.takeStatusTypeCode !== ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE || hasRole(Roles.SYSTEM_ADMINISTRATOR); return (
- {canEditTake && ( - } - onClick={() => { - setModalContent({ - ...getDeleteModalProps(), - handleOk: () => { - onRemove(takeIndex); - setDisplayModal(false); - }, - }); - setDisplayModal(true); - }} - > - )} -