diff --git a/backend/api/Areas/Admin/Controllers/ParcelController.cs b/backend/api/Areas/Admin/Controllers/ParcelController.cs index 9396919a74..ca1638ca82 100644 --- a/backend/api/Areas/Admin/Controllers/ParcelController.cs +++ b/backend/api/Areas/Admin/Controllers/ParcelController.cs @@ -127,25 +127,6 @@ public IActionResult GetParcelByPid(string pid) public IActionResult AddParcel([FromBody] ParcelModel model) { var entity = _mapper.Map(model); - entity.Evaluations.Add(new Entity.ParcelEvaluation(model.FiscalYear, entity) // TODO: Move this logic to AutoMapper. - { - EstimatedValue = model.EstimatedValue, - AssessedValue = model.AssessedValue, - NetBookValue = model.NetBookValue - }); - - foreach (var building in model.Buildings) - { - // We only allow adding buildings at this point. Can't include an existing one. - var b_entity = _mapper.Map(building); - b_entity.Evaluations.Add(new Entity.BuildingEvaluation(building.FiscalYear, b_entity) // TODO: Move this logic to AutoMapper. - { - EstimatedValue = building.EstimatedValue, - AssessedValue = building.AssessedValue, - NetBookValue = building.NetBookValue - }); - entity.Buildings.Add(b_entity); - } _pimsAdminService.Parcel.Add(entity); var parcel = _mapper.Map(entity); @@ -162,30 +143,6 @@ public IActionResult AddParcel([FromBody] ParcelModel model) public IActionResult AddParcels([FromBody] ParcelModel[] models) { var entities = _mapper.Map(models); - - for (var i = 0; i < models.Count(); i++) - { - var entity = entities[i]; - entity.Evaluations.Add(new Entity.ParcelEvaluation(models[i].FiscalYear, entity) // TODO: Move this logic to AutoMapper. - { - EstimatedValue = models[i].EstimatedValue, - AssessedValue = models[i].AssessedValue, - NetBookValue = models[i].NetBookValue - }); - - foreach (var building in models[i].Buildings) - { - // We only allow adding buildings at this point. Can't include an existing one. - var b_entity = _mapper.Map(building); - b_entity.Evaluations.Add(new Entity.BuildingEvaluation(building.FiscalYear, b_entity) // TODO: Move this logic to AutoMapper. - { - EstimatedValue = building.EstimatedValue, - AssessedValue = building.AssessedValue, - NetBookValue = building.NetBookValue - }); - entity.Buildings.Add(b_entity); - } - } _pimsAdminService.Parcel.Add(entities); var parcels = _mapper.Map(entities); @@ -208,25 +165,28 @@ public IActionResult UpdateParcel([FromBody] ParcelModel model) _mapper.Map(model, entity); - // Update evaluation. - var p_eval = entity.Evaluations.FirstOrDefault(e => e.FiscalYear == model.FiscalYear); - if (p_eval == null) + foreach (var evaluation in model.Evaluations) { - entity.Evaluations.Add(new Entity.ParcelEvaluation(model.FiscalYear, entity) // TODO: Move this logic to AutoMapper. + // Update evaluation. + var p_eval = entity.Evaluations.FirstOrDefault(e => e.FiscalYear == evaluation.FiscalYear); + if (p_eval == null) + { + entity.Evaluations.Add(new Entity.ParcelEvaluation(evaluation.FiscalYear, entity) // TODO: Move this logic to AutoMapper. { - EstimatedValue = model.EstimatedValue, - AssessedValue = model.AssessedValue, - NetBookValue = model.NetBookValue, - CreatedById = userId + EstimatedValue = evaluation.EstimatedValue, + AssessedValue = evaluation.AssessedValue, + NetBookValue = evaluation.NetBookValue, + CreatedById = userId }); - } - else - { - p_eval.EstimatedValue = model.EstimatedValue; - p_eval.AssessedValue = model.AssessedValue; - p_eval.NetBookValue = model.NetBookValue; - p_eval.UpdatedById = userId; // TODO: Move to DAL. - p_eval.UpdatedOn = DateTime.UtcNow; + } + else + { + p_eval.EstimatedValue = evaluation.EstimatedValue; + p_eval.AssessedValue = evaluation.AssessedValue; + p_eval.NetBookValue = evaluation.NetBookValue; + p_eval.UpdatedById = userId; // TODO: Move to DAL. + p_eval.UpdatedOn = DateTime.UtcNow; + } } foreach (var building in model.Buildings) @@ -236,14 +196,16 @@ public IActionResult UpdateParcel([FromBody] ParcelModel model) // Add a new building to the parcel. var b_entity = _mapper.Map(building); b_entity.CreatedById = userId; - - b_entity.Evaluations.Add(new Entity.BuildingEvaluation(building.FiscalYear, b_entity) // TODO: Move this logic to AutoMapper. + foreach (var evaluation in building.Evaluations) + { + b_entity.Evaluations.Add(new Entity.BuildingEvaluation(evaluation.FiscalYear, b_entity) // TODO: Move this logic to AutoMapper. { - EstimatedValue = building.EstimatedValue, - AssessedValue = building.AssessedValue, - NetBookValue = building.NetBookValue, - CreatedById = userId + EstimatedValue = evaluation.EstimatedValue, + AssessedValue = evaluation.AssessedValue, + NetBookValue = evaluation.NetBookValue, + CreatedById = userId }); + } entity.Buildings.Add(b_entity); } @@ -260,26 +222,29 @@ public IActionResult UpdateParcel([FromBody] ParcelModel model) b_entity.UpdatedById = userId; b_entity.UpdatedOn = DateTime.UtcNow; - // Update evaluation. - var b_eval = b_entity.Evaluations.FirstOrDefault(e => e.FiscalYear == building.FiscalYear); - - if (b_eval == null) + foreach (var evaluation in building.Evaluations) { - b_entity.Evaluations.Add(new Entity.BuildingEvaluation(building.FiscalYear, b_entity) // TODO: Move this logic to AutoMapper. + // Update evaluation. + var b_eval = b_entity.Evaluations.FirstOrDefault(e => e.FiscalYear == evaluation.FiscalYear); + + if (b_eval == null) + { + b_entity.Evaluations.Add(new Entity.BuildingEvaluation(evaluation.FiscalYear, b_entity) // TODO: Move this logic to AutoMapper. { - EstimatedValue = building.EstimatedValue, - AssessedValue = building.AssessedValue, - NetBookValue = building.NetBookValue, - CreatedById = userId + EstimatedValue = evaluation.EstimatedValue, + AssessedValue = evaluation.AssessedValue, + NetBookValue = evaluation.NetBookValue, + CreatedById = userId }); - } - else - { - b_eval.EstimatedValue = building.EstimatedValue; - b_eval.AssessedValue = building.AssessedValue; - b_eval.NetBookValue = building.NetBookValue; - b_eval.UpdatedById = userId; // TODO: Move to DAL. - b_eval.UpdatedOn = DateTime.UtcNow; + } + else + { + b_eval.EstimatedValue = evaluation.EstimatedValue; + b_eval.AssessedValue = evaluation.AssessedValue; + b_eval.NetBookValue = evaluation.NetBookValue; + b_eval.UpdatedById = userId; // TODO: Move to DAL. + b_eval.UpdatedOn = DateTime.UtcNow; + } } _pimsAdminService.Building.UpdateOne(b_entity); diff --git a/backend/api/Controllers/ParcelController.cs b/backend/api/Controllers/ParcelController.cs index f2de783adb..f1ff05b6d1 100644 --- a/backend/api/Controllers/ParcelController.cs +++ b/backend/api/Controllers/ParcelController.cs @@ -67,6 +67,7 @@ public IActionResult GetParcels() /// [HttpPost] [HasPermission(Permissions.PropertyView)] + [Route("/api/parcels/filter")] public IActionResult GetParcels([FromBody]ParcelFilter filter) { filter.ThrowBadRequestIfNull($"The request must include a filter."); @@ -102,14 +103,6 @@ public IActionResult AddParcel([FromBody] ParcelModel model) var entity = _mapper.Map(model); var userId = this.User.GetUserId(); - foreach (var building in model.Buildings) - { - // We only allow adding buildings at this point. Can't include an existing one. - var b_entity = _mapper.Map(building); - b_entity.CreatedById = userId; - entity.Buildings.Add(b_entity); - } - _pimsService.Parcel.Add(entity); var parcel = _mapper.Map(entity); diff --git a/backend/api/Models/BuildingModel.cs b/backend/api/Models/BuildingModel.cs index f5833f756d..954cfeaa8d 100644 --- a/backend/api/Models/BuildingModel.cs +++ b/backend/api/Models/BuildingModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Pims.Api.Models { @@ -35,13 +36,7 @@ public class BuildingModel : BaseModel, IEquatable public float RentableArea { get; set; } - public int FiscalYear { get; set; } - - public float EstimatedValue { get; set; } - - public float AssessedValue { get; set; } - - public float NetBookValue { get; set; } + public IEnumerable Evaluations { get; set; } = new List(); public override bool Equals(object obj) { @@ -65,10 +60,7 @@ public bool Equals([AllowNull] BuildingModel other) BuildingPredominateUse == other.BuildingPredominateUse && BuildingTenancy == other.BuildingTenancy && RentableArea == other.RentableArea && - FiscalYear == other.FiscalYear && - EstimatedValue == other.EstimatedValue && - AssessedValue == other.AssessedValue && - NetBookValue == other.NetBookValue; + Enumerable.SequenceEqual(Evaluations, other.Evaluations); } public override int GetHashCode() @@ -88,10 +80,7 @@ public override int GetHashCode() hash.Add(BuildingPredominateUse); hash.Add(BuildingTenancy); hash.Add(RentableArea); - hash.Add(FiscalYear); - hash.Add(EstimatedValue); - hash.Add(AssessedValue); - hash.Add(NetBookValue); + hash.Add(Evaluations); return hash.ToHashCode(); } #endregion diff --git a/backend/api/Models/EvaluationModel.cs b/backend/api/Models/EvaluationModel.cs new file mode 100644 index 0000000000..da294038bf --- /dev/null +++ b/backend/api/Models/EvaluationModel.cs @@ -0,0 +1,49 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Pims.Api.Models +{ + public class EvaluationModel : BaseModel, IEquatable + { + #region Properties + public int PropertyId { get; set; } + + public int FiscalYear { get; set; } + + public float EstimatedValue { get; set; } + + public float AssessedValue { get; set; } + + public float NetBookValue { get; set; } + + public override bool Equals(object obj) + { + return Equals(obj as EvaluationModel); + } + + public bool Equals([AllowNull] EvaluationModel other) + { + return other != null && + base.Equals(other) && + PropertyId == other.PropertyId && + FiscalYear == other.FiscalYear && + EstimatedValue == other.EstimatedValue && + AssessedValue == other.AssessedValue && + NetBookValue == other.NetBookValue; + } + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(base.GetHashCode()); + hash.Add(PropertyId); + hash.Add(FiscalYear); + hash.Add(EstimatedValue); + hash.Add(AssessedValue); + hash.Add(NetBookValue); + return hash.ToHashCode(); + } + + #endregion + } +} diff --git a/backend/api/Models/ParcelModel.cs b/backend/api/Models/ParcelModel.cs index 59719ce1d6..da050262c2 100644 --- a/backend/api/Models/ParcelModel.cs +++ b/backend/api/Models/ParcelModel.cs @@ -30,14 +30,6 @@ public class ParcelModel : BaseModel, IEquatable public AddressModel Address { get; set; } - public int FiscalYear { get; set; } - - public float EstimatedValue { get; set; } - - public float AssessedValue { get; set; } - - public float NetBookValue { get; set; } - public double Latitude { get; set; } public double Longitude { get; set; } @@ -48,6 +40,7 @@ public class ParcelModel : BaseModel, IEquatable public string LandLegalDescription { get; set; } + public IEnumerable Evaluations { get; set; } = new List(); public IEnumerable Buildings { get; set; } = new List(); public override bool Equals(object obj) @@ -70,16 +63,13 @@ public bool Equals([AllowNull] ParcelModel other) SubAgency == other.SubAgency && Agency == other.Agency && EqualityComparer.Default.Equals(Address, other.Address) && - FiscalYear == other.FiscalYear && - EstimatedValue == other.EstimatedValue && - AssessedValue == other.AssessedValue && - NetBookValue == other.NetBookValue && Latitude == other.Latitude && Longitude == other.Longitude && LandArea == other.LandArea && Description == other.Description && LandLegalDescription == other.LandLegalDescription && - Enumerable.SequenceEqual(Buildings, other.Buildings); + Enumerable.SequenceEqual(Buildings, other.Buildings) && + Enumerable.SequenceEqual(Evaluations, other.Evaluations); } public override int GetHashCode() @@ -97,16 +87,13 @@ public override int GetHashCode() hash.Add(SubAgency); hash.Add(Agency); hash.Add(Address); - hash.Add(FiscalYear); - hash.Add(EstimatedValue); - hash.Add(AssessedValue); - hash.Add(NetBookValue); hash.Add(Latitude); hash.Add(Longitude); hash.Add(LandArea); hash.Add(Description); hash.Add(LandLegalDescription); hash.Add(Buildings); + hash.Add(Evaluations); return hash.ToHashCode(); } diff --git a/backend/api/Models/Parts/ParcelBuildingModel.cs b/backend/api/Models/Parts/ParcelBuildingModel.cs index 86b11178b8..daac319f1e 100644 --- a/backend/api/Models/Parts/ParcelBuildingModel.cs +++ b/backend/api/Models/Parts/ParcelBuildingModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Pims.Api.Models.Parts { @@ -9,6 +10,10 @@ public class ParcelBuildingModel : BaseModel, IEquatable #region Properties public int Id { get; set; } + public int ParcelId { get; set; } + + public int AgencyId { get; set; } + public string LocalId { get; set; } public string Description { get; set; } @@ -33,13 +38,7 @@ public class ParcelBuildingModel : BaseModel, IEquatable public float RentableArea { get; set; } - public int FiscalYear { get; set; } - - public float EstimatedValue { get; set; } - - public float AssessedValue { get; set; } - - public float NetBookValue { get; set; } + public IEnumerable Evaluations { get; set; } = new List(); public override bool Equals(object obj) { @@ -51,6 +50,8 @@ public bool Equals([AllowNull] ParcelBuildingModel other) return other != null && Id == other.Id && LocalId == other.LocalId && + ParcelId == other.ParcelId && + AgencyId == other.AgencyId && Description == other.Description && EqualityComparer.Default.Equals(Address, other.Address) && Latitude == other.Latitude && @@ -62,10 +63,7 @@ public bool Equals([AllowNull] ParcelBuildingModel other) BuildingPredominateUse == other.BuildingPredominateUse && BuildingTenancy == other.BuildingTenancy && RentableArea == other.RentableArea && - FiscalYear == other.FiscalYear && - EstimatedValue == other.EstimatedValue && - AssessedValue == other.AssessedValue && - NetBookValue == other.NetBookValue; + Enumerable.SequenceEqual(Evaluations, other.Evaluations); } public override int GetHashCode() @@ -73,6 +71,8 @@ public override int GetHashCode() var hash = new HashCode(); hash.Add(Id); hash.Add(LocalId); + hash.Add(ParcelId); + hash.Add(AgencyId); hash.Add(Description); hash.Add(Address); hash.Add(Latitude); @@ -84,10 +84,7 @@ public override int GetHashCode() hash.Add(BuildingPredominateUse); hash.Add(BuildingTenancy); hash.Add(RentableArea); - hash.Add(FiscalYear); - hash.Add(EstimatedValue); - hash.Add(AssessedValue); - hash.Add(NetBookValue); + hash.Add(Evaluations); return hash.ToHashCode(); } #endregion diff --git a/backend/api/Profiles/BuildingProfile.cs b/backend/api/Profiles/BuildingProfile.cs index 1813dc3c15..9ff77ac1a1 100644 --- a/backend/api/Profiles/BuildingProfile.cs +++ b/backend/api/Profiles/BuildingProfile.cs @@ -30,28 +30,19 @@ public BuildingProfile() CreateMap() .ForMember(dest => dest.BuildingConstructionType, opt => opt.MapFrom(src => src.BuildingConstructionType.Name)) - .ForMember(dest => dest.FiscalYear, opt => opt.MapFrom(src => src.Evaluation.FiscalYear)) - .ForMember(dest => dest.AssessedValue, opt => opt.MapFrom(src => src.Evaluation.AssessedValue)) - .ForMember(dest => dest.EstimatedValue, opt => opt.MapFrom(src => src.Evaluation.EstimatedValue)) - .ForMember(dest => dest.NetBookValue, opt => opt.MapFrom(src => src.Evaluation.NetBookValue)) .ForMember(dest => dest.BuildingPredominateUse, opt => opt.MapFrom(src => src.BuildingPredominateUse.Name)); - CreateMap() // TODO: Map evaluation + CreateMap() .ForMember(dest => dest.BuildingConstructionType, opt => opt.Ignore()) .ForMember(dest => dest.BuildingPredominateUse, opt => opt.Ignore()) .ForMember(dest => dest.AddressId, opt => opt.MapFrom(src => src.Address.Id)) - .ForMember(dest => dest.ParcelId, opt => opt.Ignore()) .ForMember(dest => dest.Parcel, opt => opt.Ignore()) - .ForMember(dest => dest.AgencyId, opt => opt.Ignore()) .ForMember(dest => dest.Agency, opt => opt.Ignore()) .ForMember(dest => dest.IsSensitive, opt => opt.Ignore()) + .ForMember(dest => dest.Evaluations, opt => opt.MapFrom(src => src.Evaluations)) .IncludeBase(); CreateMap() - .ForMember(dest => dest.FiscalYear, opt => opt.MapFrom(src => src.Evaluation.FiscalYear)) - .ForMember(dest => dest.AssessedValue, opt => opt.MapFrom(src => src.Evaluation.AssessedValue)) - .ForMember(dest => dest.EstimatedValue, opt => opt.MapFrom(src => src.Evaluation.EstimatedValue)) - .ForMember(dest => dest.NetBookValue, opt => opt.MapFrom(src => src.Evaluation.NetBookValue)) .ForMember(dest => dest.BuildingConstructionType, opt => opt.MapFrom(src => src.BuildingConstructionType.Name)) .ForMember(dest => dest.BuildingPredominateUse, opt => opt.MapFrom(src => src.BuildingPredominateUse.Name)); diff --git a/backend/api/Profiles/EvaluationProfile.cs b/backend/api/Profiles/EvaluationProfile.cs new file mode 100644 index 0000000000..99199d6f28 --- /dev/null +++ b/backend/api/Profiles/EvaluationProfile.cs @@ -0,0 +1,30 @@ +using AutoMapper; +using Pims.Api.Models; +using Entity = Pims.Dal.Entities; + +namespace Pims.Api.Helpers.Profiles +{ + public class EvaluationProfile : Profile + { + #region Constructors + public EvaluationProfile() + { + CreateMap() + .ForMember(dest => dest.PropertyId, opt => opt.MapFrom(src => src.ParcelId)) + .IncludeBase(); + + CreateMap() + .ForMember(dest => dest.ParcelId, opt => opt.MapFrom(src => src.PropertyId)) + .IncludeBase(); + + CreateMap() + .ForMember(dest => dest.PropertyId, opt => opt.MapFrom(src => src.BuildingId)) + .IncludeBase(); + + CreateMap() + .ForMember(dest => dest.BuildingId, opt => opt.MapFrom(src => src.PropertyId)) + .IncludeBase(); + } + #endregion + } +} diff --git a/backend/api/Profiles/ParcelProfile.cs b/backend/api/Profiles/ParcelProfile.cs index 6eb4ce507e..222bb789e8 100644 --- a/backend/api/Profiles/ParcelProfile.cs +++ b/backend/api/Profiles/ParcelProfile.cs @@ -28,10 +28,6 @@ public ParcelProfile() CreateMap() .ForMember(dest => dest.PID, opt => opt.MapFrom(src => src.ParcelIdentity)) - .ForMember(dest => dest.FiscalYear, opt => opt.MapFrom(src => src.Evaluation.FiscalYear)) - .ForMember(dest => dest.EstimatedValue, opt => opt.MapFrom(src => src.Evaluation.EstimatedValue)) - .ForMember(dest => dest.AssessedValue, opt => opt.MapFrom(src => src.Evaluation.AssessedValue)) - .ForMember(dest => dest.NetBookValue, opt => opt.MapFrom(src => src.Evaluation.NetBookValue)) .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status.Name)) .ForMember(dest => dest.Classification, opt => opt.MapFrom(src => src.Classification.Name)) .ForMember(dest => dest.Agency, opt => opt.ConvertUsing(new ParcelAgencyConverter())) @@ -46,7 +42,8 @@ public ParcelProfile() .ForMember(dest => dest.Agency, opt => opt.Ignore()) .ForMember(dest => dest.AddressId, opt => opt.MapFrom(src => src.Address.Id)) .ForMember(dest => dest.IsSensitive, opt => opt.Ignore()) - .ForMember(dest => dest.Buildings, opt => opt.Ignore()) + .ForMember(dest => dest.Buildings, opt => opt.MapFrom(src => src.Buildings)) + .ForMember(dest => dest.Evaluations, opt => opt.MapFrom(src => src.Evaluations)) .IncludeBase(); } #endregion diff --git a/backend/dal/PIMSContext.cs b/backend/dal/PIMSContext.cs index ec292fac88..224a8690dc 100644 --- a/backend/dal/PIMSContext.cs +++ b/backend/dal/PIMSContext.cs @@ -1,3 +1,7 @@ +using System; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Pims.Dal.Configuration; using Pims.Dal.Entities; @@ -27,7 +31,9 @@ public class PimsContext : DbContext public DbSet Provinces { get; set; } public DbSet Users { get; set; } public DbSet Roles { get; set; } - public DbSet Claims { get; set; } + public DbSet Claims { get; set; } + + private readonly IHttpContextAccessor _httpContextAccessor; #endregion #region Constructors @@ -45,9 +51,9 @@ public PimsContext() /// /// /// - public PimsContext(DbContextOptions options) : base(options) + public PimsContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor = null) : base(options) { - + _httpContextAccessor = httpContextAccessor; } #endregion @@ -103,6 +109,31 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } + public override int SaveChanges() + { + // get entries that are being Added or Updated + var modifiedEntries = ChangeTracker.Entries() + .Where(x => (x.State == EntityState.Added || x.State == EntityState.Modified)); + var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value; + if (userId != null) + { + foreach (var entry in modifiedEntries) + { + var entity = entry.Entity as BaseEntity; + + if (entry.State == EntityState.Added) + { + entity.CreatedById = new Guid(userId); + entity.CreatedOn = DateTime.UtcNow; + } + entity.UpdatedById = new Guid(userId); + entity.UpdatedOn = DateTime.UtcNow; + } + } + + return base.SaveChanges(); + } + /// /// Wrap the save changes in a transaction for rollback. /// diff --git a/backend/dal/Services/Concrete/ParcelService.cs b/backend/dal/Services/Concrete/ParcelService.cs index db5440a1b4..03f21c2ed6 100644 --- a/backend/dal/Services/Concrete/ParcelService.cs +++ b/backend/dal/Services/Concrete/ParcelService.cs @@ -161,6 +161,7 @@ public Parcel GetNoTracking(int id) .Include(p => p.Buildings).ThenInclude(b => b.Address.Province) .Include(p => p.Buildings).ThenInclude(b => b.BuildingConstructionType) .Include(p => p.Buildings).ThenInclude(b => b.BuildingPredominateUse) + .Include(p => p.Buildings).ThenInclude(b => b.Evaluations) .AsNoTracking() .FirstOrDefault(p => p.Id == id && (!p.IsSensitive || (viewSensitive && userAgencies.Contains(p.AgencyId)))) ?? throw new KeyNotFoundException(); @@ -200,25 +201,106 @@ public Parcel Add(Parcel parcel) /// public Parcel Update(Parcel parcel) { - parcel.ThrowIfNotAllowedToEdit(nameof(parcel), this.User, Permissions.PropertyEdit); + var existingParcel = this.Context.Parcels + .Include(p => p.Agency) + .Include(p => p.Address) + .Include(p => p.Evaluations) + .Include(p => p.Buildings).ThenInclude(b => b.Evaluations) + .Include(p => p.Buildings).ThenInclude(b => b.Address) + .SingleOrDefault(u => u.Id == parcel.Id) ?? throw new KeyNotFoundException(); - var entity = this.Context.Parcels.Find(parcel.Id) ?? throw new KeyNotFoundException(); + parcel.ThrowIfNotAllowedToEdit(nameof(parcel), this.User, "property-edit"); var userAgencies = this.User.GetAgencies(); - if (!userAgencies.Contains(entity.AgencyId)) throw new NotAuthorizedException("User may not edit parcels outside of their agency."); + if (!userAgencies.Contains(parcel.AgencyId)) throw new NotAuthorizedException("User may not edit parcels outside of their agency."); // Do not allow switching agencies through this method. - if (entity.AgencyId != parcel.AgencyId) throw new NotAuthorizedException("Parcel cannot be transferred to the specified agency."); + if (existingParcel.AgencyId != parcel.AgencyId) throw new NotAuthorizedException("Parcel cannot be transferred to the specified agency."); this.Context.Parcels.ThrowIfNotUnique(parcel); - this.Context.Entry(entity).CurrentValues.SetValues(parcel); - entity.UpdatedById = this.User.GetUserId(); - entity.UpdatedOn = DateTime.UtcNow; - - this.Context.Parcels.Update(entity); + //Add/Update a parcel and all child collections + if (existingParcel == null) + { + this.Context.Add(parcel); + } + else + { + this.Context.Entry(existingParcel).CurrentValues.SetValues(parcel); + this.Context.Entry(existingParcel.Address).CurrentValues.SetValues(parcel.Address); + foreach (var building in parcel.Buildings) + { + var existingBuilding = existingParcel.Buildings + .FirstOrDefault(b => b.Id == building.Id); + this.Context.Entry(existingBuilding.Address).CurrentValues.SetValues(building.Address); + if (existingBuilding == null) + { + existingParcel.Buildings.Add(building); + } + else + { + this.Context.Entry(existingBuilding).CurrentValues.SetValues(building); + + foreach (var buildingEvaluation in building.Evaluations) + { + var existingBuildingEvaluation = existingBuilding.Evaluations + .FirstOrDefault(e => e.BuildingId == buildingEvaluation.BuildingId && e.FiscalYear == buildingEvaluation.FiscalYear); + + if (existingBuildingEvaluation == null) + { + existingBuilding.Evaluations.Add(buildingEvaluation); + } + else + { + this.Context.Entry(existingBuildingEvaluation).CurrentValues.SetValues(buildingEvaluation); + } + } + } + } + foreach (var parcelEvaluation in parcel.Evaluations) + { + var existingEvaluation = existingParcel.Evaluations + .FirstOrDefault(e => e.ParcelId == parcelEvaluation.ParcelId && e.FiscalYear == parcelEvaluation.FiscalYear); + + if (existingEvaluation == null) + { + existingParcel.Evaluations.Add(parcelEvaluation); + } + else + { + this.Context.Entry(existingEvaluation).CurrentValues.SetValues(parcelEvaluation); + } + } + } + + //Delete any missing records in child collections. + foreach (var building in existingParcel.Buildings) + { + var matchingBuilding = parcel.Buildings.FirstOrDefault(b => b.Id == building.Id); + if (matchingBuilding == null) + { + this.Context.Buildings.Remove(building); + continue; + } + foreach (var buildingEvaluation in building.Evaluations) + { + if (!matchingBuilding.Evaluations.Any(e => (e.BuildingId == buildingEvaluation.BuildingId && e.FiscalYear == buildingEvaluation.FiscalYear))) + { + this.Context.BuildingEvaluations.Remove(buildingEvaluation); + } + } + } + foreach (var parcelEvaluation in existingParcel.Evaluations) + { + if (!parcel.Evaluations.Any(e => (e.ParcelId == parcelEvaluation.ParcelId && e.FiscalYear == parcelEvaluation.FiscalYear))) + { + this.Context.ParcelEvaluations.Remove(parcelEvaluation); + } + } + + this.Context.SaveChanges(); this.Context.CommitTransaction(); - return entity; + return parcel; } ///