using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using MMDAL.CustomCode.Indent; using MMDAL.CustomCode.ProductionPlan; using MMDAL.DTO.Indent; using MMDAL.DTO.ProductionPlan; #pragma warning disable CS8604 using Newtonsoft.Json; using static GB5Shared.GB5Constant.Constant; namespace MMBLL.ProductionPlan { public class ProductionPlanBLL : IProductionPlanBLL { private readonly IProductionPlanDAL _dal; private readonly IIndentDAL _indentDal; private readonly AutoNumber _autoNumber; private readonly IQueryExecutor _queryExecutor; private readonly KeyInvalidate _keyInvalidate; private readonly BaseEntityAppService _baseEntityAppService; private readonly ILogger _logger; public ProductionPlanBLL( IProductionPlanDAL dal, IIndentDAL indentDal, AutoNumber autoNumber, IQueryExecutor queryExecutor, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService, ILogger logger) { _dal = dal; _indentDal = indentDal; _autoNumber = autoNumber; _queryExecutor = queryExecutor; _keyInvalidate = keyInvalidate; _baseEntityAppService = baseEntityAppService; _logger = logger; } public async Task GetSelectListProductionPlan(CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct) => await _dal.GetSelectListProductionPlan(criteriaDTO, login, ct).ConfigureAwait(false); public async Task GetSelectListProductionPlanDetail(CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct) => await _dal.GetSelectListProductionPlanDetail(criteriaDTO, login, ct).ConfigureAwait(false); public async Task SaveProductionPlan(ProductionPlanSaveDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-productionplan", new { dto.ProductionPlanId }); if (dto.OUId == 0)throw new ArgumentException("OUId is required"); if (dto.PeriodId == 0)throw new ArgumentException("PeriodId is required"); if (dto.BizTransactionTypeId == 0)throw new ArgumentException("BizTransactionTypeId is required"); bool isNew = dto.ProductionPlanId == 0; dto.TenantId = login.ClientId; dto.ModifiedById = login.UserId; if (isNew) { // NEW : Generate ProductionPlanId var planIdAuto = await _autoNumber.GetNumberAsync(1, "PRODUCTIONPLAN", login).ConfigureAwait(false); dto.ProductionPlanId = Convert.ToInt32(planIdAuto.StartNumber); // Existing Production Plan Number generation var planNum = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.PRODUCTIONPLAN, login).ConfigureAwait(false); dto.ProductionPlanNumber = planNum.StartNumber.ToString(); dto.CreatedById = login.UserId; } // Stamp detail TenantId + SlNo short slNo = 1; foreach (var d in dto.Details) { d.TenantId = login.ClientId; d.SlNo = slNo++; } GB5Trace.Step("save-productionplan", new { dto.ProductionPlanId, isNew }); await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { int planId; if (isNew) { // Save header await _dal.SaveProductionPlanHeader(dto, login, tx, ct).ConfigureAwait(false); planId = dto.ProductionPlanId; } else { planId = dto.ProductionPlanId; await _dal.UpdateProductionPlanHeader(dto, login, tx, ct).ConfigureAwait(false); await _dal.DeleteProductionPlanDetails(planId, login, tx, ct).ConfigureAwait(false); } foreach (var d in dto.Details) { d.ProductionPlanId = planId; if (d.ProductionPlanDetailId == 0) { var detailId = await _autoNumber.GetNumberAsync(1, "PRODUCTIONPLAN", login).ConfigureAwait(false); d.ProductionPlanDetailId = Convert.ToInt32(detailId.StartNumber); } } if (dto.Details.Any()) { await _dal.BulkSaveProductionPlanDetails(dto.Details, login, tx).ConfigureAwait(false); } await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } GB5Trace.Step("event-publish",new { EventTypeConstant.SAVEPRODUCTIONPLANEVENTTYPEID }); var cacheKey = new CacheKeyGeneration().KeyGeneration(dto.ProductionPlanId,EntityConstant.OBJECTPRODUCTIONPLAN,CacheKeyLevel.CLIENT_LEVEL,login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.ProductionPlanId}" : SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("save-productionplan-failed", ex); _logger.LogError(ex,"SaveProductionPlan failed for ProductionPlanId {Id}",dto.ProductionPlanId); throw; } } public async Task GetProductionPlan(int productionPlanId, LoginDTO login, CancellationToken ct) { var header = await _dal.GetProductionPlan(productionPlanId, login, ct).ConfigureAwait(false); if (header is null) return JsonConvert.SerializeObject(null); var details = await _dal.GetProductionPlanDetails(productionPlanId, login, ct).ConfigureAwait(false); header.Details = [.. details]; return JsonConvert.SerializeObject(header); } public async Task GetProductionPlanList(int firstNumber, int maxResult, LoginDTO login, CancellationToken ct) { var (items, count) = await _dal.GetProductionPlanList(firstNumber, maxResult, login, ct).ConfigureAwait(false); return JsonConvert.SerializeObject(new { Items = items, TotalCount = count }); } public async Task DeleteProductionPlan(int productionPlanId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-productionplan", new { productionPlanId }); await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _dal.DeleteProductionPlanDetails(productionPlanId, login, tx, ct).ConfigureAwait(false); await _dal.SoftDeleteProductionPlan(productionPlanId, login.UserId, login, tx, ct).ConfigureAwait(false); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } var cacheKey = new CacheKeyGeneration().KeyGeneration(productionPlanId, EntityConstant.OBJECTPRODUCTIONPLAN, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("delete-productionplan-failed", ex); _logger.LogError(ex, "DeleteProductionPlan failed for ProductionPlanId {Id}", productionPlanId); throw; } } public async Task GenerateWorkOrdersFromPlan(int productionPlanId, int bizTransactionTypeId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("generate-wo-from-plan", new { productionPlanId }); var planDetails = await _dal.GetPlanDetailsForWOGeneration(productionPlanId, login, ct).ConfigureAwait(false); var detailList = planDetails.ToList(); if (detailList.Count == 0) return "No unlinked plan detail lines found to generate work orders."; var plan = await _dal.GetProductionPlan(productionPlanId, login, ct).ConfigureAwait(false); if (plan is null) throw new InvalidOperationException($"Production plan {productionPlanId} not found."); int generated = 0; foreach (var planDetail in detailList) { // Pre-assign IDs outside transaction — auto-number is not transactional var indentNum = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.INDENT, login).ConfigureAwait(false); var detailNum = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.INDENT, login).ConfigureAwait(false); int indentId = indentNum.StartNumber; int indentDetailId = detailNum.StartNumber; // RoutingId was never populated here — every generated work order left the // Routing/Workstation resolution chain (WI's MWIASSIGNMENT dimension match, // MLINEOPERATIONMAP) with nothing to resolve against. RoutingDetailId (the // Process-specific step) is deliberately left unset here — it's resolved later // once a line/workstation is actually chosen, not something a bulk plan-to-WO // generator can know up front. int routingId = await _indentDal.GetItemDefaultRoutingId(planDetail.ItemId, login, ct).ConfigureAwait(false); var woDto = new IndentSaveDTO { IndentId = indentId, IndentNumber = indentId.ToString(), OUId = plan.OUId, PeriodId = plan.PeriodId, BizTransactionTypeId = bizTransactionTypeId, IndentDate = planDetail.FromDate, ProcessId = plan.ProcessId, WorkCenterId = plan.WorkCenterId, SourceType = 2, // 2=ProductionPlan ExpectedCompletionDate = planDetail.ToDate }; var detailRow = new IndentDetailInsertDTO { IndentDetailId = indentDetailId, IndentId = indentId, SlNo = 1, ItemId = planDetail.ItemId, SkuId = planDetail.SKUId, RoutingId = routingId, IndentDetailQuantity = (decimal)planDetail.BatchTotalQuantity, IndentDetailPriority = planDetail.Priority, IndentDetailReleaseDate = planDetail.FromDate, TenantId = login.ClientId }; await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _indentDal.SaveIndentHeader(woDto, login, tx, ct).ConfigureAwait(false); await _indentDal.BulkSaveIndentDetails([detailRow], login, tx, ct).ConfigureAwait(false); await _dal.LinkPlanDetailToIndent(planDetail.ProductionPlanDetailId, indentDetailId, login, tx, ct).ConfigureAwait(false); await tx.CommitAsync(ct).ConfigureAwait(false); generated++; } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } } GB5Trace.Step("generate-wo-from-plan-done", new { generated }); _logger.LogInformation("GenerateWorkOrdersFromPlan: {Count} WOs generated for PlanId {PlanId}", generated, productionPlanId); return $"{generated} work order(s) generated successfully."; } catch (Exception ex) { GB5Trace.MarkFailed("generate-wo-from-plan-failed", ex); _logger.LogError(ex, "GenerateWorkOrdersFromPlan failed for PlanId {Id}", productionPlanId); throw; } } } }