using GB5Shared.DaprCache; 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.Routing; using MMDAL.DTO.Routing; using Newtonsoft.Json; using static GB5Shared.GB5Constant.Constant; namespace MMBLL.Routing { public class RoutingBLL : IRoutingBLL { private readonly IRoutingDAL _dal; private readonly AutoNumber _autoNumber; private readonly IQueryExecutor _qe; private readonly KeyInvalidate _keyInvalidate; private readonly BaseEntityAppService _entityApp; private readonly ILogger _logger; public RoutingBLL( IRoutingDAL dal, AutoNumber autoNumber, IQueryExecutor qe, KeyInvalidate keyInvalidate, BaseEntityAppService entityApp, ILogger logger) { _dal = dal; _autoNumber = autoNumber; _qe = qe; _keyInvalidate = keyInvalidate; _entityApp = entityApp; _logger = logger; } // ── Picklist (existing) ─────────────────────────────────────────────── public async Task GetSelectListRouting(CriteriaDTO criteriaDTO, LoginDTO loginDTO) { try { return await _dal.GetSelectListRouting(criteriaDTO, loginDTO).ConfigureAwait(false); } catch (Exception) { throw; } } public async Task GetSelectListRoutingVersion(int firstNumber, int maxResult, LoginDTO login, CancellationToken ct) { try { return await _dal.GetSelectListRoutingVersion(firstNumber, maxResult, login, ct).ConfigureAwait(false); } catch (Exception) { throw; } } // ── Get ─────────────────────────────────────────────────────────────── public async Task GetRouting(int routingId, LoginDTO login, CancellationToken ct) { var header = await _dal.GetRoutingHeader(routingId, login, ct).ConfigureAwait(false); if (header is null) return JsonConvert.SerializeObject(null); header.Versions = (await _dal.GetRoutingVersions(routingId, login, ct).ConfigureAwait(false)).ToList(); foreach (var v in header.Versions) v.Details = (await _dal.GetRoutingVersionDetails(v.RoutingVersionId, login, ct).ConfigureAwait(false)).ToList(); return JsonConvert.SerializeObject(header); } public async Task GetRoutingList(int firstNumber, int maxResult, LoginDTO login, CancellationToken ct) { var (items, total) = await _dal.GetRoutingList(firstNumber, maxResult, login, ct).ConfigureAwait(false); return JsonConvert.SerializeObject(new { Items = items, TotalCount = total }); } // ── Save ────────────────────────────────────────────────────────────── public async Task SaveRouting(RoutingSaveDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-routing", new { dto.RoutingId }); if (string.IsNullOrWhiteSpace(dto.RoutingCode)) throw new ArgumentException("Routing code is required."); if (string.IsNullOrWhiteSpace(dto.RoutingName)) throw new ArgumentException("Routing name is required."); bool isNew = dto.RoutingId == 0; if (isNew) { var hNum = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.MROUTING, login).ConfigureAwait(false); dto.RoutingId = hNum.StartNumber; } dto.TenantId = login.ClientId; // Pre-assign version and detail IDs foreach (var v in dto.Versions) { if (v.RoutingVersionId == 0) { var vNum = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.MROUTINGVERSION, login).ConfigureAwait(false); v.RoutingVersionId = vNum.StartNumber; } v.RoutingId = dto.RoutingId; v.TenantId = login.ClientId; if (v.Details.Count > 0) { var dNum = await _autoNumber.GetNumberAsync(v.Details.Count, AUTONUMBERCONSTANT.MROUTINGDETAIL, login).ConfigureAwait(false); int nextId = dNum.StartNumber; for (short i = 0; i < v.Details.Count; i++) { v.Details[i].RoutingDetailId = nextId++; v.Details[i].RoutingVersionId = v.RoutingVersionId; v.Details[i].TenantId = login.ClientId; } } } GB5Trace.Step("save-routing", new { dto.RoutingId, isNew }); await _entityApp.ExecuteSaveAsync( EntityConstant.OBJECTROUTING, EventTypeConstant.SAVEROUTINGEVENTTYPEID, dto, login, async tx => { if (isNew) await _dal.SaveRoutingHeader(dto, login, tx, ct).ConfigureAwait(false); else await _dal.UpdateRoutingHeader(dto, login, tx, ct).ConfigureAwait(false); foreach (var v in dto.Versions) { await _dal.DeleteRoutingVersionDetails(v.RoutingVersionId, login, tx, ct).ConfigureAwait(false); await _dal.SaveRoutingVersion(v, login, tx, ct).ConfigureAwait(false); if (v.Details.Count > 0) await _dal.BulkSaveRoutingVersionDetails(v.Details, login, tx).ConfigureAwait(false); } return dto.RoutingId; }).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SAVEROUTINGEVENTTYPEID }); var saveKey = new CacheKeyGeneration().KeyGeneration(dto.RoutingId, EntityConstant.OBJECTROUTING, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(saveKey).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.RoutingId}" : SuccessResponse.UpdateSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("save-routing-failed", ex); _logger.LogError(ex, "SaveRouting failed for RoutingId {RoutingId}", dto.RoutingId); throw; } } // ── Delete ──────────────────────────────────────────────────────────── public async Task DeleteRouting(int routingId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-routing", new { routingId }); var stub = new RoutingSaveDTO { RoutingId = routingId, TenantId = login.ClientId }; await _entityApp.ExecuteSaveAsync( EntityConstant.OBJECTROUTING, EventTypeConstant.DELETEROUTINGEVENTTYPEID, stub, login, async tx => { await _dal.SoftDeleteRouting(routingId, login, tx, ct).ConfigureAwait(false); return routingId; }).ConfigureAwait(false); var delKey = new CacheKeyGeneration().KeyGeneration(routingId, EntityConstant.OBJECTROUTING, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(delKey).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("delete-routing-failed", ex); _logger.LogError(ex, "DeleteRouting failed for RoutingId {RoutingId}", routingId); throw; } } // ── ChangeProcess ───────────────────────────────────────────────────── public async Task ChangeProcess(int routingId, int newProcessId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("change-process-routing", new { routingId, newProcessId }); var header = await _dal.GetRoutingHeader(routingId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"RoutingId {routingId} not found."); header.ProcessId = newProcessId; header.TenantId = login.ClientId; var stub = new RoutingSaveDTO { RoutingId = routingId, ProcessId = newProcessId, TenantId = login.ClientId }; await _entityApp.ExecuteSaveAsync( EntityConstant.OBJECTROUTING, EventTypeConstant.SAVEROUTINGEVENTTYPEID, stub, login, async tx => { await _dal.UpdateRoutingHeader(header, login, tx, ct).ConfigureAwait(false); return routingId; }).ConfigureAwait(false); var cpKey = new CacheKeyGeneration().KeyGeneration(routingId, EntityConstant.OBJECTROUTING, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cpKey).ConfigureAwait(false); return SuccessResponse.UpdateSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("change-process-routing-failed", ex); _logger.LogError(ex, "ChangeProcess failed for RoutingId {RoutingId}", routingId); throw; } } } }