using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Validation; using System.Text.Json; using WiDAL.CustomCode.WiMaster; using WiDAL.DTOs; namespace WiBLL.WiMaster; public class WiMasterBLL : IWiMasterBLL { private readonly IWiMasterDAL _dal; private readonly AutoNumber _autoNumber; private readonly IValidation _validation; // Allowed status transitions: key = current, value = valid next statuses private static readonly Dictionary AllowedTransitions = new() { [WiStatus.Draft] = [WiStatus.InReview], [WiStatus.InReview] = [WiStatus.Approved, WiStatus.Draft], [WiStatus.Approved] = [WiStatus.Published], [WiStatus.Published] = [WiStatus.Archived], [WiStatus.Archived] = [] }; public WiMasterBLL(IWiMasterDAL dal, AutoNumber autoNumber, IValidation validation) { _dal = dal; _autoNumber = autoNumber; _validation = validation; } // ── Queries ────────────────────────────────────────────────────────────── public async Task GetWiList(int wiProcessId, byte wiStatus, LoginDTO login, CancellationToken ct) => await _dal.GetWiList(wiProcessId, wiStatus, login, ct).ConfigureAwait(false); public async Task GetWiById(int wiId, LoginDTO login, CancellationToken ct) { var wi = await _dal.GetWiById(wiId, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(wi); } public async Task GetWiFullDetail(int wiId, LoginDTO login, CancellationToken ct) => await _dal.GetWiFullDetail(wiId, login, ct).ConfigureAwait(false); public async Task GetVersionHistory(int wiId, LoginDTO login, CancellationToken ct) => await _dal.GetVersionHistory(wiId, login, ct).ConfigureAwait(false); public async Task GetVersionSnapshot(int lwiVersionId, LoginDTO login, CancellationToken ct) => await _dal.GetVersionSnapshot(lwiVersionId, login, ct).ConfigureAwait(false); // ── FullSave ─────────────────────────────────────────────────────────── public async Task SaveWiFull(WiSaveRequestDTO req,LoginDTO login,CancellationToken ct) { // ── Guards ──────────────────────────────────────────────────────────── if (req is null) throw new ArgumentNullException(nameof(req)); if (req.Header is null) throw new ArgumentNullException(nameof(req.Header)); if (login is null) throw new ArgumentNullException(nameof(login)); await _validation.NotEmpty(req.Header.WiCode, nameof(req.Header.WiCode)); await _validation.NotEmpty(req.Header.WiTitle, nameof(req.Header.WiTitle)); if (req.Header.WiSubProcessId == 0) throw new InvalidOperationException("WiSubProcessId is required."); // ── Stamp login info ────────────────────────────────────────────────── req.Header.TenantId = login.ClientId; req.Header.ModifiedById = login.UserId; bool isNew = req.Header.WiId == 0; AutoNumberDTO? an = null; try { if (isNew) { // Count how many IDs we need int sectionCount = req.Sections?.Count ?? 0; int stepCount = req.Sections? .SelectMany(s => s.Steps ?? []) .Count() ?? 0; int contentCount = req.Sections? .SelectMany(s => s.Steps ?? []) .SelectMany(s => s.Contents ?? []) .Count() ?? 0; int skillCount = req.SkillRequirements?.Count ?? 0; int competencyCount = req.CompetencyRequirements?.Count ?? 0; // 1 (header) + all children int totalIds = 1 + sectionCount + stepCount + contentCount + skillCount + competencyCount; // Own entity code — was "WIVERSION" (a copy-paste from WI.LWIVERSION's own // AutoNumber usage in WiVersionBLL.Save), silently draining the version-log's // counter to generate an unrelated PK. See 20260816_WI_MAUTONUMBER_Seed_*.sql. an = await _autoNumber.GetNumberAsync(totalIds, "WI", login); int nextId = an.StartNumber; // Assign IDs top-down req.Header.WiId = nextId++; req.Header.CreatedById = login.UserId; foreach (var section in req.Sections ?? []) { section.WiId = req.Header.WiId; section.WiSectionId = nextId++; foreach (var step in section.Steps ?? []) { step.WiId = req.Header.WiId; step.WiSectionId = section.WiSectionId; step.WiStepId = nextId++; foreach (var content in step.Contents ?? []) { content.WiStepId = step.WiStepId; content.WiStepContentId = nextId++; } } } foreach (var skill in req.SkillRequirements ?? []) { skill.WiId = req.Header.WiId; skill.WiSkillRequirementId = nextId++; } foreach (var competency in req.CompetencyRequirements ?? []) { competency.WiId = req.Header.WiId; competency.WiCompetencyRequirementId = nextId++; } } await _dal.SaveWiFull(req, login, ct); return isNew ? string.Format(SuccessResponse.SaveSuccessCode, req.Header.WiCode) : string.Format(SuccessResponse.UpdateSuccessCode, req.Header.WiCode); } catch { // Roll back the reserved number block on failure if (an is not null && isNew) await _autoNumber.RollbackAutoNumber("WI", an.StartNumber, login); throw; } } // ── Status transitions ──────────────────────────────────────────────────── public async Task SubmitForReview(int wiId, LoginDTO login, CancellationToken ct) => await TransitionStatus(wiId, WiStatus.InReview, login, ct).ConfigureAwait(false); public async Task ApproveWi(int wiId, LoginDTO login, CancellationToken ct) => await TransitionStatus(wiId, WiStatus.Approved, login, ct).ConfigureAwait(false); public async Task PublishWi(int wiId, LoginDTO login, CancellationToken ct) => await TransitionStatus(wiId, WiStatus.Published, login, ct).ConfigureAwait(false); public async Task ArchiveWi(int wiId, LoginDTO login, CancellationToken ct) => await TransitionStatus(wiId, WiStatus.Archived, login, ct).ConfigureAwait(false); // ── Helpers ─────────────────────────────────────────────────────────────── private async Task TransitionStatus( int wiId, WiStatus targetStatus, LoginDTO login, CancellationToken ct) { var wi = await _dal.GetWiById(wiId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"WI {wiId} not found."); var currentStatus = Enum.IsDefined(typeof(WiStatus), wi.WiStatus) ? (WiStatus)wi.WiStatus : throw new InvalidOperationException($"Invalid WI status value: {wi.WiStatus}"); if (currentStatus == targetStatus) { return $"WI is already in {targetStatus} status."; } if (!AllowedTransitions[currentStatus].Contains(targetStatus)) throw new InvalidOperationException( $"Cannot transition from {currentStatus} to {targetStatus}."); // Snapshot the full content tree as of this transition — content itself is untouched by // a status change, only SaveWiFull mutates it, so this reflects exactly what was live at // the moment of transition. Does not cover edits made between transitions while in Draft. // Read before the transaction: it's a pure read of already-committed content, unaffected // by the status/version-log writes below. var snapshotJson = await _dal.GetWiFullDetail(wiId, login, ct).ConfigureAwait(false); // Status update + version-log append + snapshot insert must succeed or fail together — // a partial failure here would leave WISTATUS updated with no corresponding history row. await _dal.TransitionStatusWithSnapshot(wiId, (byte)targetStatus, login.UserId, new WiVersionLogDTO { WiId = wiId, WiVersion = wi.WiVersion, WiStatus = targetStatus, ChangeSummary = $"Status changed to {targetStatus}", ChangedById = login.UserId, // This row IS the new current state the moment it's written — TransitionStatusWithSnapshot // clears any previous ISCURRENT=1 row for this WiId in the same transaction. IsCurrent = true, CreatedById = login.UserId, CreatedOn = DateTime.UtcNow }, snapshotJson, login, ct).ConfigureAwait(false); return $"{SuccessResponse.UpdateSuccessMessage} {wiId}"; } // ── Public static utility (used by WiAssignmentBLL and WiExecutionBLL's resolve-time // dimension annotation — one shared weight table so scoring and transparency never drift) ── public static readonly (string Dimension, Func Selector, short Weight)[] SpecificityDimensions = [ ("RoutingOperationId", d => d.RoutingOperationId, 30), ("EntityId", d => d.EntityId, 25), ("RoutingId", d => d.RoutingId, 20), ("WorkStationId", d => d.WorkStationId, 15), ("WorkCenterId", d => d.WorkCenterId, 10), ("ModelId", d => d.ModelId, 10), ("ItemGroupId", d => d.ItemGroupId, 5) ]; public static short ComputeSpecificityScore(WiAssignmentDTO dto) => (short)SpecificityDimensions.Sum(d => d.Selector(dto) != -1 ? d.Weight : 0); public async Task GetSelectListWiMaster(CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct) => await _dal.GetSelectListWiMaster(criteriaDTO, login, ct); public async Task DeleteWiMaster(int WiId, LoginDTO loginDTO, CancellationToken ct) { try { return await _dal.DeleteWiMaster(WiId, loginDTO, ct); } catch (Exception) { throw; } } public async Task GetStepsWithWorkstationAsync(int wiId, LoginDTO login, CancellationToken ct) => await _dal.GetStepsWithWorkstationAsync(wiId, login, ct).ConfigureAwait(false); }