Skip to content

Commit

Permalink
Allow Products with multiple projects (#3578)
Browse files Browse the repository at this point in the history
* Updated backend and frontend to allow to reuse products on multiple projects

* Fixed delete flow

* fixed lint

* PR comments and fixes

* Feedback enhancements

* Updated test snaps
  • Loading branch information
FuriousLlama authored Nov 8, 2023
1 parent 82cd1cf commit 8b6330a
Show file tree
Hide file tree
Showing 37 changed files with 449 additions and 279 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using MapsterMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -8,6 +9,7 @@
using Pims.Api.Services;
using Pims.Core.Exceptions;
using Pims.Core.Json;
using Pims.Dal.Exceptions;
using Pims.Dal.Security;
using Swashbuckle.AspNetCore.Annotations;

Expand Down Expand Up @@ -113,11 +115,11 @@ public IActionResult GetAll()
[ProducesResponseType(typeof(ProjectModel), 200)]
[ProducesResponseType(typeof(Api.Models.ErrorResponseModel), 400)]
[SwaggerOperation(Tags = new[] { "project" })]
public IActionResult AddProject(ProjectModel projectModel)
public IActionResult AddProject(ProjectModel projectModel, [FromQuery] string[] userOverrideCodes)
{
try
{
var newProject = _projectService.Add(_mapper.Map<Dal.Entities.PimsProject>(projectModel));
var newProject = _projectService.Add(_mapper.Map<Dal.Entities.PimsProject>(projectModel), userOverrideCodes.Select(oc => UserOverrideCode.Parse(oc)));
return new JsonResult(_mapper.Map<ProjectModel>(newProject));
}
catch (DuplicateEntityException e)
Expand All @@ -135,7 +137,7 @@ public IActionResult AddProject(ProjectModel projectModel)
[Produces("application/json")]
[ProducesResponseType(typeof(ProjectModel), 200)]
[SwaggerOperation(Tags = new[] { "project" })]
public IActionResult UpdateProject([FromRoute] long id, [FromBody] ProjectModel model)
public IActionResult UpdateProject([FromRoute] long id, [FromBody] ProjectModel model, [FromQuery] string[] userOverrideCodes)
{
if (id != model.Id)
{
Expand All @@ -144,7 +146,7 @@ public IActionResult UpdateProject([FromRoute] long id, [FromBody] ProjectModel

try
{
var updatedProject = _projectService.Update(_mapper.Map<Dal.Entities.PimsProject>(model));
var updatedProject = _projectService.Update(_mapper.Map<Dal.Entities.PimsProject>(model), userOverrideCodes.Select(oc => UserOverrideCode.Parse(oc)));
return new JsonResult(updatedProject);
}
catch (DuplicateEntityException e)
Expand Down
5 changes: 2 additions & 3 deletions source/backend/api/Models/Concepts/Product/ProductMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ public void Register(TypeAdapterConfig config)
{
config.NewConfig<Entity.PimsProduct, ProductModel>()
.Map(dest => dest.Id, src => src.Internal_Id)
//.Map(dest => dest.ParentProject, src => src.ParentProject) // Todo: Fix This.
//.Map(dest => dest.ParentProjectId, src => src.ParentProjectId) // Todo: Fix This.
.Map(dest => dest.ProjectProducts, src => src.PimsProjectProducts)
.Map(dest => dest.AcquisitionFiles, src => src.PimsAcquisitionFiles)
.Map(dest => dest.Code, src => src.Code)
.Map(dest => dest.Description, src => src.Description)
Expand All @@ -23,7 +22,7 @@ public void Register(TypeAdapterConfig config)

config.NewConfig<ProductModel, Entity.PimsProduct>()
.Map(dest => dest.Internal_Id, src => src.Id)
//.Map(dest => dest.ParentProjectId, src => src.ParentProjectId) // Todo: Fix This.
.Map(dest => dest.PimsProjectProducts, src => src.ProjectProducts)
.Map(dest => dest.PimsAcquisitionFiles, src => src.AcquisitionFiles)
.Map(dest => dest.Code, src => src.Code)
.Map(dest => dest.Description, src => src.Description)
Expand Down
13 changes: 6 additions & 7 deletions source/backend/api/Models/Concepts/Product/ProductModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

namespace Pims.Api.Models.Concepts
{
/*
* Front end model
* LINK @frontend/src\models\api\Project.ts
*/
public class ProductModel : BaseAppModel
{
#region Properties
Expand All @@ -13,14 +17,9 @@ public class ProductModel : BaseAppModel
public long? Id { get; set; }

/// <summary>
/// get/set - The project associated to this product.
/// get/set - The project associations to this product.
/// </summary>
public virtual ProjectModel ParentProject { get; set; }

/// <summary>
/// get/set - The project's id.
/// </summary>
public long? ParentProjectId { get; set; }
public List<ProjectProductModel> ProjectProducts { get; set; }

/// <summary>
/// get/set - The product associated files.
Expand Down
4 changes: 2 additions & 2 deletions source/backend/api/Models/Concepts/Project/ProjectMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void Register(TypeAdapterConfig config)
.Map(dest => dest.Code, src => src.Code)
.Map(dest => dest.Description, src => src.Description)
.Map(dest => dest.Note, src => src.Note)
//.Map(dest => dest.Products, src => src.PimsProducts) // TODO: Fix this.
.Map(dest => dest.ProjectProducts, src => src.PimsProjectProducts)
.Map(dest => dest.AppLastUpdateUserid, src => src.AppLastUpdateUserid)
.Map(dest => dest.AppLastUpdateTimestamp, src => src.AppLastUpdateTimestamp)
.Inherits<Entity.IBaseEntity, BaseModel>();
Expand All @@ -32,7 +32,7 @@ public void Register(TypeAdapterConfig config)
.Map(dest => dest.Code, src => src.Code)
.Map(dest => dest.Description, src => src.Description)
.Map(dest => dest.Note, src => src.Note)
//.Map(dest => dest.PimsProducts, src => src.Products) // TODO: Fix this.
.Map(dest => dest.PimsProjectProducts, src => src.ProjectProducts)
.Inherits<BaseModel, Entity.IBaseEntity>();
}
}
Expand Down
6 changes: 5 additions & 1 deletion source/backend/api/Models/Concepts/Project/ProjectModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

namespace Pims.Api.Models.Concepts
{
/*
* Frontend model
* LINK @frontend/src\models\api\Project.ts:10
*/
public class ProjectModel : BaseAppModel
{
#region Properties
Expand Down Expand Up @@ -54,7 +58,7 @@ public class ProjectModel : BaseAppModel
/// <summary>
/// get/set - Project products.
/// </summary>
public List<ProductModel> Products { get; set; }
public List<ProjectProductModel> ProjectProducts { get; set; }
#endregion
}
}
26 changes: 26 additions & 0 deletions source/backend/api/Models/Concepts/Project/ProjectProductMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Mapster;
using Entity = Pims.Dal.Entities;

namespace Pims.Api.Models.Concepts
{
public class ProjectProductMap : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<Entity.PimsProjectProduct, ProjectProductModel>()
.Map(dest => dest.Id, src => src.ProjectProductId)
.Map(dest => dest.ProjectId, src => src.ProjectId)
.Map(dest => dest.Product, src => src.Product)
.Map(dest => dest.ProductId, src => src.ProductId)
.Map(dest => dest.Project, src => src.Project)
.Inherits<Entity.IBaseEntity, BaseModel>();

config.NewConfig<ProjectProductModel, Entity.PimsProjectProduct>()
.Map(dest => dest.ProjectProductId, src => src.Id)
.Map(dest => dest.ProjectId, src => src.ProjectId)
.Map(dest => dest.ProductId, src => src.Product.Id)
.Map(dest => dest.Product, src => src.Product)
.Inherits<BaseModel, Entity.IBaseEntity>();
}
}
}
24 changes: 24 additions & 0 deletions source/backend/api/Models/Concepts/Project/ProjectProductModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

namespace Pims.Api.Models.Concepts
{
/*
* Frontend model
* LINK @frontend/src\models\api\Project.ts
*/
public class ProjectProductModel : BaseAppModel
{
#region Properties

public long Id { get; set; }

public long ProjectId { get; set; }

public ProductModel Product { get; set; }

public long ProductId { get; set; }

public ProjectModel Project { get; set; }

#endregion
}
}
9 changes: 5 additions & 4 deletions source/backend/api/Services/IProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Pims.Dal.Entities;
using Pims.Dal.Entities.Models;
using Pims.Dal.Exceptions;

namespace Pims.Api.Services
{
Expand All @@ -13,14 +14,14 @@ public interface IProjectService

Task<Paged<PimsProject>> GetPage(ProjectFilter filter);

PimsProject Add(PimsProject project);

PimsProject GetById(long projectId);

IList<PimsProduct> GetProducts(long projectId);

List<PimsAcquisitionFile> GetProductFiles(long productId);

PimsProject Update(PimsProject project);
PimsProject Add(PimsProject project, IEnumerable<UserOverrideCode> userOverrides);

PimsProject Update(PimsProject project, IEnumerable<UserOverrideCode> userOverrides);
}
}
}
97 changes: 82 additions & 15 deletions source/backend/api/Services/ProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Pims.Core.Exceptions;
using Pims.Core.Extensions;
using Pims.Dal.Entities;
using Pims.Dal.Entities.Models;
using Pims.Dal.Exceptions;
using Pims.Dal.Helpers.Extensions;
using Pims.Dal.Repositories;
using Pims.Dal.Security;
Expand Down Expand Up @@ -125,7 +126,7 @@ public List<PimsAcquisitionFile> GetProductFiles(long productId)
return _acquisitionFileRepository.GetByProductId(productId);
}

public PimsProject Add(PimsProject project)
public PimsProject Add(PimsProject project, IEnumerable<UserOverrideCode> userOverrides)
{
_user.ThrowIfNotAuthorized(Permissions.ProjectAdd);
_logger.LogInformation("Adding new project...");
Expand All @@ -134,44 +135,109 @@ public PimsProject Add(PimsProject project)
throw new ArgumentNullException(nameof(project), "Project cannot be null.");
}

// TODO: Verify this is not needed
//CheckForDuplicateProducts(project.PimsProducts, project.Id);
var externalProducts = MatchProducts(project);
if (externalProducts.Count > 0 && !userOverrides.Contains(UserOverrideCode.ProductReuse))
{
var names = externalProducts.Select(x => $"{x.Code} {x.Description}");
throw new UserOverrideException(UserOverrideCode.ProductReuse, $"The product(s) {string.Join(",", names)} can also be found in one or more other projects. Please verify the correct product is being added");
}

var newProject = _projectRepository.Add(project);
_projectRepository.CommitTransaction();

return _projectRepository.Get(newProject.Internal_Id);
}

public PimsProject Update(PimsProject project)
public PimsProject Update(PimsProject project, IEnumerable<UserOverrideCode> userOverrides)
{
_user.ThrowIfNotAuthorized(Permissions.ProjectEdit);
project.ThrowIfNull(nameof(project));
_logger.LogInformation($"Updating project with id ${project.Internal_Id}");

// TODO: Verify this is not needed
//CheckForDuplicateProducts(project.PimsProducts, project.Id);
var externalProducts = MatchProducts(project);
if (externalProducts.Count > 0 && !userOverrides.Contains(UserOverrideCode.ProductReuse))
{
var names = externalProducts.Select(x => $"{x.Code} {x.Description}");
throw new UserOverrideException(UserOverrideCode.ProductReuse, $"The product(s) {string.Join(",", names)} can also be found in one or more other projects. Please verify the correct product is being added");
}

var updatedProject = _projectRepository.Update(project);

AddNoteIfStatusChanged(project);
_projectRepository.CommitTransaction();

return updatedProject;
}

/*private void CheckForDuplicateProducts(IEnumerable<PimsProduct> products, long projectId)
/*
* Updates the passed project with the matched products in place.
* Note: The return list contains the external products matched
*/
private List<PimsProduct> MatchProducts(PimsProject project)
{
var duplicateProductsInArray = products.GroupBy(p => (p.Code, p.Description)).Where(g => g.Count() > 1).Select(g => g.Key);
if (duplicateProductsInArray.Any())
var existingProjectProducts = _productRepository.GetProjectProductsByProject(project.Id);

var matchedProjectProducts = new List<PimsProjectProduct>();
var notMatched = new List<PimsProjectProduct>();

var externalProducts = new List<PimsProduct>();

// Try to match from the existing relationship by product code and description
foreach (var projectProduct in project.PimsProjectProducts)
{
throw new DuplicateEntityException($"Unable to add project with duplicated product codes: {string.Join(", ", duplicateProductsInArray.Select(dp => dp.Code))}");
var existing = existingProjectProducts.FirstOrDefault(x => x.Product.Code == projectProduct.Product.Code && x.Product.Description == projectProduct.Product.Description);
if (existing != null)
{
// Manually update the members with the new data
existing.Product.StartDate = projectProduct.Product.StartDate;
existing.Product.CostEstimate = projectProduct.Product.CostEstimate;
existing.Product.CostEstimateDate = projectProduct.Product.CostEstimateDate;
existing.Product.Objective = projectProduct.Product.Objective;
existing.Product.Scope = projectProduct.Product.Scope;

projectProduct.Product = existing.Product;

matchedProjectProducts.Add(existing);
}
else
{
notMatched.Add(projectProduct);
}
}

IEnumerable<PimsProduct> duplicateProducts = _productRepository.GetByProductBatch(products, projectId);
if (duplicateProducts.Any())
var existingProducts = _productRepository.GetProducts(notMatched.Select(x => x.Product));

// Try to match from the existing products code and description
foreach (var projectProduct in notMatched)
{
throw new DuplicateEntityException($"Unable to add project with duplicated product codes: {string.Join(", ", duplicateProducts.Select(dp => dp.Code))}");
var existing = existingProducts.FirstOrDefault(x => x.Code == projectProduct.Product.Code && x.Description == projectProduct.Product.Description);
if (existing != null)
{
var updatedProduct = existing;

// Manually update the members with the new data
updatedProduct.StartDate = projectProduct.Product.StartDate;
updatedProduct.CostEstimate = projectProduct.Product.CostEstimate;
updatedProduct.CostEstimateDate = projectProduct.Product.CostEstimateDate;
updatedProduct.Objective = projectProduct.Product.Objective;
updatedProduct.Scope = projectProduct.Product.Scope;

projectProduct.Product = updatedProduct;
projectProduct.ProductId = updatedProduct.Id;
projectProduct.ProjectId = project.Id;

externalProducts.Add(existing);
}

// Add the updated to the list of matched ones.
matchedProjectProducts.Add(projectProduct);
}
}*/

// Populate the products with the matched data
project.PimsProjectProducts = matchedProjectProducts;

return externalProducts;
}

private async Task<Paged<PimsProject>> GetPageAsync(ProjectFilter filter, IEnumerable<short> userRegions)
{
Expand Down Expand Up @@ -232,3 +298,4 @@ private string GetUpdatedNoteText(string oldStatusCode, string newStatusCode)
}
}
}

17 changes: 13 additions & 4 deletions source/backend/dal/Exceptions/OverrideExceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,22 @@ public static UserOverrideCode ContractorSelfRemoved
get { return new UserOverrideCode("CONTRACTOR_SELFREMOVED"); }
}

public string Code { get; private set; }

private static List<UserOverrideCode> UserOverrideCodes
public static UserOverrideCode ProductReuse
{
get { return new List<UserOverrideCode>() { UserOverrideCode.AddPropertyToInventory, UserOverrideCode.AddLocationToProperty, UserOverrideCode.UpdateRegion, UserOverrideCode.PoiToInventory }; }
get { return new UserOverrideCode("PRODUCT_REUSE"); }
}

public string Code { get; private set; }

private static List<UserOverrideCode> UserOverrideCodes => new List<UserOverrideCode>() {

Check warning on line 40 in source/backend/dal/Exceptions/OverrideExceptions.cs

View workflow job for this annotation

GitHub Actions / build-backend

Braces for multi-line statements should not share line
UserOverrideCode.AddPropertyToInventory,
UserOverrideCode.AddLocationToProperty,
UserOverrideCode.UpdateRegion,
UserOverrideCode.PoiToInventory,
UserOverrideCode.ContractorSelfRemoved,
UserOverrideCode.ProductReuse,
};

private UserOverrideCode(string code)
{
Code = code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ public interface IProductRepository : IRepository<PimsProduct>
{
IList<PimsProduct> GetByProject(long projectId);

IList<PimsProjectProduct> GetProjectProductsByProject(long projectId);

IEnumerable<PimsProduct> GetProducts(IEnumerable<PimsProduct> incomingProducts);

IEnumerable<PimsProduct> GetByProductBatch(IEnumerable<PimsProduct> incomingProducts, long projectId);
}
}
Loading

0 comments on commit 8b6330a

Please sign in to comment.