Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.vs/
obj/
.dotnet/
core/domain/bin/
core/application/bin
6 changes: 3 additions & 3 deletions Core/Application/Application.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand All @@ -14,8 +14,8 @@
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions Core/Application/Common/Repositories/IEntityDbSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public interface IEntityDbSet
public DbSet<UnitMeasure> UnitMeasure { get; set; }
public DbSet<ProductGroup> ProductGroup { get; set; }
public DbSet<Product> Product { get; set; }
public DbSet<BillOfMaterial> BillOfMaterials { get; set; }
public DbSet<BillOfMaterialItem> BillOfMaterialItems { get; set; }
public DbSet<CustomerContact> CustomerContact { get; set; }
public DbSet<VendorContact> VendorContact { get; set; }
public DbSet<Tax> Tax { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using Application.Common.Repositories;
using Application.Features.BillOfMaterialManager;
using Domain.Entities;
using FluentValidation;
using MediatR;

namespace Application.Features.BillOfMaterialItemManager.Commands;

public class CreateBillOfMaterialItemResult
{
public BillOfMaterialItem? Data { get; set; }
}

public class CreateBillOfMaterialItemRequest : IRequest<CreateBillOfMaterialItemResult>
{
public string? BillOfMaterialId { get; init; }
public string? ComponentProductId { get; init; }
public double? Quantity { get; init; } = 1;
public int? Sequence { get; init; }
public string? UnitMeasureId { get; init; }
public double? ScrapPercentage { get; init; } = 0;
public string? Notes { get; init; }
public string? CreatedById { get; init; }
}

public class CreateBillOfMaterialItemValidator : AbstractValidator<CreateBillOfMaterialItemRequest>
{
public CreateBillOfMaterialItemValidator()
{
RuleFor(x => x.BillOfMaterialId).NotEmpty();
RuleFor(x => x.ComponentProductId).NotEmpty();
RuleFor(x => x.Quantity).NotNull().GreaterThan(0);
RuleFor(x => x.ScrapPercentage).GreaterThanOrEqualTo(0).LessThanOrEqualTo(100);
}
}

public class CreateBillOfMaterialItemHandler : IRequestHandler<CreateBillOfMaterialItemRequest, CreateBillOfMaterialItemResult>
{
private readonly ICommandRepository<BillOfMaterialItem> _repository;
private readonly ICommandRepository<BillOfMaterial> _bomRepository;
private readonly ICommandRepository<Product> _productRepository;
private readonly BillOfMaterialService _bomService;
private readonly IUnitOfWork _unitOfWork;

public CreateBillOfMaterialItemHandler(
ICommandRepository<BillOfMaterialItem> repository,
ICommandRepository<BillOfMaterial> bomRepository,
ICommandRepository<Product> productRepository,
BillOfMaterialService bomService,
IUnitOfWork unitOfWork)
{
_repository = repository;
_bomRepository = bomRepository;
_productRepository = productRepository;
_bomService = bomService;
_unitOfWork = unitOfWork;
}

public async Task<CreateBillOfMaterialItemResult> Handle(CreateBillOfMaterialItemRequest request, CancellationToken cancellationToken)
{
// Validate BOM exists
var bom = await _bomRepository.GetAsync(request.BillOfMaterialId ?? string.Empty, cancellationToken);
if (bom == null)
{
throw new Exception($"BOM not found: {request.BillOfMaterialId}");
}

// Validate component product exists
var componentProduct = await _productRepository.GetAsync(request.ComponentProductId ?? string.Empty, cancellationToken);
if (componentProduct == null)
{
throw new Exception($"Component product not found: {request.ComponentProductId}");
}

// Circular reference validation
var parentProductId = bom.ProductId ?? string.Empty;
var componentProductId = request.ComponentProductId ?? string.Empty;
var isCircular = await _bomService.ValidateCircularReferenceAsync(parentProductId, componentProductId, cancellationToken);
if (isCircular)
{
throw new ValidationException($"Circular reference detected: product {parentProductId} cannot contain component {componentProductId}");
}

// Auto-increment sequence if not provided
int? sequence = request.Sequence;
if (!sequence.HasValue)
{
var existingItems = _repository.GetQuery().Where(x => x.BillOfMaterialId == request.BillOfMaterialId && !x.IsDeleted);
var maxSeq = existingItems.Any() ? existingItems.Max(x => x.Sequence ?? 0) : 0;
sequence = maxSeq + 1;
}

var entity = new BillOfMaterialItem
{
CreatedById = request.CreatedById,
BillOfMaterialId = request.BillOfMaterialId,
ComponentProductId = request.ComponentProductId,
Quantity = request.Quantity ?? 1,
Sequence = sequence,
UnitMeasureId = request.UnitMeasureId,
ScrapPercentage = request.ScrapPercentage ?? 0,
Notes = request.Notes
};

await _repository.CreateAsync(entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);

return new CreateBillOfMaterialItemResult { Data = entity };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Application.Common.Repositories;
using Domain.Entities;
using FluentValidation;
using MediatR;

namespace Application.Features.BillOfMaterialItemManager.Commands;

public class DeleteBillOfMaterialItemResult
{
public BillOfMaterialItem? Data { get; set; }
}

public class DeleteBillOfMaterialItemRequest : IRequest<DeleteBillOfMaterialItemResult>
{
public string? Id { get; init; }
public string? DeletedById { get; init; }
}

public class DeleteBillOfMaterialItemValidator : AbstractValidator<DeleteBillOfMaterialItemRequest>
{
public DeleteBillOfMaterialItemValidator()
{
RuleFor(x => x.Id).NotEmpty();
}
}

public class DeleteBillOfMaterialItemHandler : IRequestHandler<DeleteBillOfMaterialItemRequest, DeleteBillOfMaterialItemResult>
{
private readonly ICommandRepository<BillOfMaterialItem> _repository;
private readonly IUnitOfWork _unitOfWork;

public DeleteBillOfMaterialItemHandler(
ICommandRepository<BillOfMaterialItem> repository,
IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}

public async Task<DeleteBillOfMaterialItemResult> Handle(DeleteBillOfMaterialItemRequest request, CancellationToken cancellationToken)
{
var entity = await _repository.GetAsync(request.Id ?? string.Empty, cancellationToken);
if (entity == null)
{
throw new Exception($"BOM item not found: {request.Id}");
}

entity.UpdatedById = request.DeletedById;
_repository.Delete(entity);
await _unitOfWork.SaveAsync(cancellationToken);

return new DeleteBillOfMaterialItemResult { Data = entity };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Application.Common.Repositories;
using Domain.Entities;
using FluentValidation;
using MediatR;

namespace Application.Features.BillOfMaterialItemManager.Commands;

public class UpdateBillOfMaterialItemResult
{
public BillOfMaterialItem? Data { get; set; }
}

public class UpdateBillOfMaterialItemRequest : IRequest<UpdateBillOfMaterialItemResult>
{
public string? Id { get; init; }
public double? Quantity { get; init; }
public int? Sequence { get; init; }
public string? UnitMeasureId { get; init; }
public double? ScrapPercentage { get; init; }
public string? Notes { get; init; }
public string? UpdatedById { get; init; }
}

public class UpdateBillOfMaterialItemValidator : AbstractValidator<UpdateBillOfMaterialItemRequest>
{
public UpdateBillOfMaterialItemValidator()
{
RuleFor(x => x.Id).NotEmpty();
RuleFor(x => x.Quantity).GreaterThan(0).When(x => x.Quantity.HasValue);
RuleFor(x => x.ScrapPercentage).InclusiveBetween(0, 100).When(x => x.ScrapPercentage.HasValue);
}
}

public class UpdateBillOfMaterialItemHandler : IRequestHandler<UpdateBillOfMaterialItemRequest, UpdateBillOfMaterialItemResult>
{
private readonly ICommandRepository<BillOfMaterialItem> _repository;
private readonly IUnitOfWork _unitOfWork;

public UpdateBillOfMaterialItemHandler(
ICommandRepository<BillOfMaterialItem> repository,
IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}

public async Task<UpdateBillOfMaterialItemResult> Handle(UpdateBillOfMaterialItemRequest request, CancellationToken cancellationToken)
{
var entity = await _repository.GetAsync(request.Id ?? string.Empty, cancellationToken);
if (entity == null)
{
throw new Exception($"BOM item not found: {request.Id}");
}

entity.UpdatedById = request.UpdatedById;
if (request.Quantity.HasValue) entity.Quantity = request.Quantity;
if (request.Sequence.HasValue) entity.Sequence = request.Sequence;
if (!string.IsNullOrWhiteSpace(request.UnitMeasureId)) entity.UnitMeasureId = request.UnitMeasureId;
if (request.ScrapPercentage.HasValue) entity.ScrapPercentage = request.ScrapPercentage;
if (request.Notes != null) entity.Notes = request.Notes;

_repository.Update(entity);
await _unitOfWork.SaveAsync(cancellationToken);

return new UpdateBillOfMaterialItemResult { Data = entity };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Application.Common.CQS.Queries;
using AutoMapper;
using Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Application.Features.BillOfMaterialItemManager.Queries;

public record GetBillOfMaterialItemListDto
{
public string? Id { get; init; }
public string? BillOfMaterialId { get; init; }
public string? ComponentProductId { get; init; }
public string? ComponentProductName { get; init; }
public string? ComponentProductNumber { get; init; }
public double? Quantity { get; init; }
public int? Sequence { get; init; }
public string? UnitMeasureId { get; init; }
public string? UnitMeasureName { get; init; }
public double? ScrapPercentage { get; init; }
public string? Notes { get; init; }
}

public class GetBillOfMaterialItemListProfile : Profile
{
public GetBillOfMaterialItemListProfile()
{
CreateMap<BillOfMaterialItem, GetBillOfMaterialItemListDto>()
.ForMember(dest => dest.ComponentProductName, opt => opt.MapFrom(src => src.ComponentProduct != null ? src.ComponentProduct.Name : string.Empty))
.ForMember(dest => dest.ComponentProductNumber, opt => opt.MapFrom(src => src.ComponentProduct != null ? src.ComponentProduct.Number : string.Empty))
.ForMember(dest => dest.UnitMeasureName, opt => opt.MapFrom(src => src.UnitMeasure != null ? src.UnitMeasure.Name : string.Empty));
}
}

public class GetBillOfMaterialItemListResult
{
public List<GetBillOfMaterialItemListDto>? Data { get; init; }
}

public class GetBillOfMaterialItemListRequest : IRequest<GetBillOfMaterialItemListResult>
{
public string? BillOfMaterialId { get; init; }
}

public class GetBillOfMaterialItemListHandler : IRequestHandler<GetBillOfMaterialItemListRequest, GetBillOfMaterialItemListResult>
{
private readonly IMapper _mapper;
private readonly IQueryContext _context;

public GetBillOfMaterialItemListHandler(IMapper mapper, IQueryContext context)
{
_mapper = mapper;
_context = context;
}

public async Task<GetBillOfMaterialItemListResult> Handle(GetBillOfMaterialItemListRequest request, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(request.BillOfMaterialId))
{
return new GetBillOfMaterialItemListResult { Data = new List<GetBillOfMaterialItemListDto>() };
}

var entities = await _context.BillOfMaterialItems
.AsNoTracking()
.Where(x => !x.IsDeleted && x.BillOfMaterialId == request.BillOfMaterialId)
.Include(x => x.ComponentProduct)
.Include(x => x.UnitMeasure)
.OrderBy(x => x.Sequence ?? int.MaxValue)
.ToListAsync(cancellationToken);

var dtos = _mapper.Map<List<GetBillOfMaterialItemListDto>>(entities);

return new GetBillOfMaterialItemListResult { Data = dtos };
}
}
Loading