using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GB5Constant; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Validation; using MMDAL.CustomCode.BOM; using MMDAL.DTO.BOM; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MMBLL.BOM { public class BOMBLL : IBOMBLL { private readonly IBOMDAL _BOMDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; public BOMBLL(IBOMDAL IBOMDAL, AutoNumber autoNumber, IValidation validation) { _BOMDAL = IBOMDAL; _AutoNumber = autoNumber; _Validation = validation; } public async Task GetSelectListBOM(CriteriaDTO criteriaDTO, LoginDTO loginDTO) { try { return await _BOMDAL.GetSelectListBOM(criteriaDTO, loginDTO); } catch (Exception) { throw; } } public async Task GetSelectListBOMDetailsForTransaction(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { try { return await _BOMDAL.GetSelectListBOMDetailsForTransaction(FirstNumber, MaxResult, CriteriaDTO, LoginDTO); } catch (Exception) { throw; } } public Task GetSelectListBOMDetailsForTransaction(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO, bool Count = false) { throw new NotImplementedException(); } public async Task GetBOM(int BOMId, LoginDTO LoginDTO) { try { return await _BOMDAL.GetBOM(BOMId, LoginDTO); } catch (Exception) { throw; } } public async Task> GetBOMTree(int bomId, decimal quantityForBom, LoginDTO loginDTO, CancellationToken ct = default) { return await _BOMDAL.GetBOMTree(bomId, quantityForBom, loginDTO, ct).ConfigureAwait(false); } public async Task SaveBOM(BOMDTO dto, LoginDTO login) { if (dto == null) throw new ArgumentNullException(nameof(dto)); AutoNumberDTO? autoNumberDTO = null; try { // ── 1. Basic Validation ────────────────────────────── await _Validation.NotEmpty(dto.BOMName, nameof(dto.BOMName)); // ── 2. Normalize ToDate ────────────────────────────── // Old rule: if ToDate is 1999-12 it means "open-ended" → set to 9999-12-31 if (dto.BOMToDate.Year == 1999 && dto.BOMToDate.Month == 12) dto.BOMToDate = new DateTime(9999, 12, 31); // ── 3. FromDate ≤ ToDate guard ─────────────────────── if (dto.BOMFromDate > dto.BOMToDate) throw new InvalidOperationException("FromDate must be less than or equal to ToDate."); // ── 4. Assembly-type back-date guard ───────────────── // AssemblyType == 1 cannot be saved with a past FromDate (new records only) if (dto.AssemblyType == 1 && dto.BOMId == 0 && dto.BOMFromDate.Date < DateTime.Today) throw new InvalidOperationException("Assembly Type BOM cannot be created with a past From Date."); // ── 5. Detail quantity consistency check ───────────── // NetQty + AdjustQty must equal FinalQty for every detail line for (int i = 0; i < dto.BOMDetailArray.Count; i++) { var d = dto.BOMDetailArray[i]; if ((d.BOMDetailAdjustQuantity + d.BOMDetailNetQuantity) != d.BOMDetailFinalQuantity) throw new InvalidOperationException( "BOM Detail NetQuantity + AdjustQuantity must equal FinalQuantity. " + $"Check line index {i}."); } // ── 5b. Cyclical-BOM guard (real-time — legacy GB4 had none) ── // A candidate component item creates a cycle if, walked forward through its own // active/default BOM chain, it ever reaches back to this BOM's own production item. var candidateItemIds = dto.BOMDetailArray.Select(d => d.BOMDetailItemId); var cycle = await _BOMDAL.CheckCyclicalBom(dto.BOMProductionItemId, candidateItemIds, login); if (cycle != null) throw new InvalidOperationException( $"Saving this BOM would create a cyclical reference: component '{cycle.ItemCode} - {cycle.ItemName}' " + $"(BOM {cycle.BomId}, line {cycle.BomDetailLineId}) eventually produces '{cycle.ProudctionItemCode} - {cycle.ProudctionItemName}', " + "which is this BOM's own production item."); // ── 6. Audit stamps ────────────────────────────────── dto.BOMModifiedById = login.UserId; dto.BOMModifiedOn = DateTime.UtcNow; // ── 7. Count total IDs needed for AutoNumber ───────── // 1 for BOM header + 1 per new detail + 1 per new parameter int count = 1; foreach (var detail in dto.BOMDetailArray) { if (detail.BOMDetailLineId == 0) count++; count += detail.BOMDetailParameterArray.Count(p => p.BOMDetailParameterId == 0); } // ── 8. Reserve AutoNumbers ─────────────────────────── autoNumberDTO = await _AutoNumber.GetAutoNumber(count, "BOM", login); int nextId = autoNumberDTO.StartNumber; // ── 9. NEW BOM ──────────────────────────────────────── if (dto.BOMId == 0) { // 9a. Version resolution await ResolveVersionAsync(dto, login); // 9b. Assign header ID dto.BOMId = nextId++; // 9c. DefaultBOM auto-assign // DefaultBOMId == -1 means the header IS the default BOM if (dto.DefaultBOMId == -1) { dto.DefaultBOMId = dto.BOMId; // Populate Code/Name from the production item when empty if (string.IsNullOrEmpty(dto.BOMCode) || string.IsNullOrEmpty(dto.BOMName)) { var item = await _BOMDAL.GetItemByIdAsync(dto.BOMProductionItemId, login); if (item != null) { if (string.IsNullOrEmpty(dto.BOMCode)) dto.BOMCode = item.ItemCode; if (string.IsNullOrEmpty(dto.BOMName)) dto.BOMName = item.ItemName; } } } // 9d. RequiredProductionQuantity default if (dto.BOMRequiredProductionQuantity == 0) dto.BOMRequiredProductionQuantity = dto.BOMProductionQty; // 9e. Open-ended ToDate for new records dto.BOMToDate = new DateTime(9999, 12, 31); // 9f. Audit create stamps dto.BOMCreatedById = login.UserId; dto.BOMCreatedOn = DateTime.UtcNow; // 9g. Save header await _BOMDAL.SaveBOMAsync(dto, login); // 9h. Save detail lines nextId = await SaveDetailLinesAsync(dto, login, nextId, true); return $"{SuccessResponse.SaveSuccessMessage} {dto.BOMId}"; } // ── 10. EXISTING BOM (UPDATE) ───────────────────────── else { // 10a. Update header await _BOMDAL.UpdateBOMAsync(dto, login); // 10b. Upsert detail lines nextId = await SaveDetailLinesAsync(dto, login, nextId, false); return $"{SuccessResponse.UpdateSuccessMessage} {dto.BOMId}"; } } catch { // Roll back reserved AutoNumbers if the BOM header ID was already assigned if (autoNumberDTO != null && dto.BOMId > 0) await _AutoNumber.RollbackAutoNumber("BOM", dto.BOMId, login); throw; } } // ── Private: Version resolution ────────────────────────────────────────── // Mirrors old logic: // • Get the latest existing version for the same item/defaultBOM/linkId. // • Validate that new FromDate > previous version FromDate. // • Increment version number (or start at "1"). // • Close the previous version's ToDate. private async Task ResolveVersionAsync(BOMDTO dto, LoginDTO login) { var previous = await _BOMDAL.GetPreviousVersionAsync(dto, login); if (previous == null) { dto.BOMVersionName = "1"; } else { if (previous.BOMFromDate >= dto.BOMFromDate) throw new InvalidOperationException( $"BOM FromDate must be greater than the previous version date " + $"({previous.BOMFromDate:dd-MMM-yyyy}). " + "Another BOM version exists — please check."); // Try to increment numeric version; fall back to "2" if non-numeric dto.BOMVersionName = int.TryParse(previous.BOMVersionName, out int ver) ? (ver + 1).ToString() : "2"; // Close previous open-ended version await _BOMDAL.UpdatePreviousVersionToDateAsync(dto, login); } } // ── Private: Detail + Parameter upsert ─────────────────────────────────── // For each detail line: // • If RequiredProductionQuantity == 0 → default it (PerQty × ProductionQty). // • Assign new IDs from the AutoNumber pool for new lines (LineId == 0). // • Save or Update accordingly. // • Do the same for every parameter under that detail line. private async Task SaveDetailLinesAsync( BOMDTO dto, LoginDTO login, int nextId, bool isNew) { foreach (var detail in dto.BOMDetailArray) { if (detail.BOMDetailRequiredProductionQuantity == 0) { detail.BOMDetailRequiredProductionQuantity = detail.BOMDetailPerProductionQuantity * dto.BOMProductionQty; } detail.BOMDetailBOMId = dto.BOMId; bool detailIsNew = detail.BOMDetailLineId == 0; if (detailIsNew) detail.BOMDetailLineId = nextId++; if (detailIsNew || isNew) await _BOMDAL.SaveBOMDetailAsync(detail, login); else await _BOMDAL.UpdateBOMDetailAsync(detail, login); foreach (var param in detail.BOMDetailParameterArray) { param.BOMDetailParameterBOMLineId = detail.BOMDetailLineId; bool paramIsNew = param.BOMDetailParameterId == 0; if (paramIsNew) param.BOMDetailParameterId = nextId++; if (paramIsNew || isNew) await _BOMDAL.SaveBOMDetailParameterAsync(param, login); else await _BOMDAL.UpdateBOMDetailParameterAsync(param, login); } } return nextId; } public async Task DeleteBOM(int BOMId, LoginDTO LoginDTO) { try { return await _BOMDAL.DeleteBOM(BOMId, LoginDTO); } catch (Exception) { throw; } } } }