using System; using System.Threading; using System.Threading.Tasks; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using GB5Shared.Promotion; using Newtonsoft.Json; using WiBLL.WiMaster; using WiDAL.CustomCode.Promotion; using WiDAL.DTOs; namespace WiBLL.Promotion { // ============================================================ // WiHeaderPromotionAdapter — IPromotableEntity for MWI ("WORKINSTRUCTION"), the actual Work // Instruction document. Parent row only — Sections/Steps/Contents/SkillRequirements/ // CompetencyRequirements are not bundled (same "children deferred as a follow-up" precedent as // ReportView's Fields/Dashboard's Pages). // // Load-bearing, not just a scope-reduction shortcut: WiMasterBLL.SaveWiFull's own DAL // implementation (checked directly, not assumed) unconditionally soft-deletes every existing // Section/Step/Content for this WiId before inserting whatever req.Sections contains — the // exact same destructive-update-on-children shape already found and fixed for TMS's // TrainingProgrammeBLL.SaveTrainingProgramme. A parent-only adapter that constructed a bare // WiSaveRequestDTO on every re-promotion would silently wipe a destination WI's real content on // every update. // // Fixed the same way as TMS Programme: on update (existingDestinationId resolves), Import first // reads the destination's own current full detail via GetWiFullDetail — its returned JSON // shape (Header/Sections/SkillRequirements/CompetencyRequirements) matches WiSaveRequestDTO's // own properties exactly, confirmed by reading WiMasterDAL.GetWiFullDetail directly — then // overlays only the promotable header fields onto it in place, leaving Sections/ // SkillRequirements/CompetencyRequirements exactly as they already are at the destination. A // fresh insert has nothing to preserve, so it starts from an empty request. // // WiProcessId/WiSubProcessId are resolved via natural-key lookups — the destination must // already have a WiProcess/WiSubProcess with matching codes (same "promote the parent hierarchy // first" precedent as PortletType before Portlet). ApprovedById is a raw UserId reference with // no code-based lookup available — conservatively blocked (RawFlags-carried for validation // only, never mapped at import, same pattern as Portlet's own blocked cross-references). // ============================================================ public class WiHeaderPromotionAdapter : IPromotableEntity { public string EntityTypeCode => "WORKINSTRUCTION"; private readonly IWiMasterBLL _WiMasterBLL; private readonly IWiProcessCodeLookupDAL _WiProcessCodeLookup; private readonly IWiSubProcessCodeLookupDAL _WiSubProcessCodeLookup; private readonly IWiHeaderCodeLookupDAL _WiHeaderCodeLookup; public WiHeaderPromotionAdapter( IWiMasterBLL wiMasterBLL, IWiProcessCodeLookupDAL wiProcessCodeLookup, IWiSubProcessCodeLookupDAL wiSubProcessCodeLookup, IWiHeaderCodeLookupDAL wiHeaderCodeLookup) { _WiMasterBLL = wiMasterBLL; _WiProcessCodeLookup = wiProcessCodeLookup; _WiSubProcessCodeLookup = wiSubProcessCodeLookup; _WiHeaderCodeLookup = wiHeaderCodeLookup; } private class PortablePayload { public string WiCode { get; set; } = string.Empty; public string WiTitle { get; set; } = string.Empty; public string WiProcessCode { get; set; } = string.Empty; public string WiSubProcessCode { get; set; } = string.Empty; public byte ActivityType { get; set; } public string? ActivitySubType { get; set; } public string WiVersion { get; set; } = "1.0"; public byte WiStatus { get; set; } public byte DisplayContext { get; set; } public byte StepAdvanceMode { get; set; } public decimal StdCycleTimeMins { get; set; } public DateTime? EffectiveDate { get; set; } public DateTime? ReviewDueDate { get; set; } public string? Tags { get; set; } public string? WiDescription { get; set; } public int IsActive { get; set; } public byte Status { get; set; } = 1; public short SortOrder { get; set; } = 9999; /// Seen only by ValidateForPromotionAsync — ImportPromotedAsync never maps /// this onto the destination row. See the class header comment. public RawFlags Flags { get; set; } = new(); } private class RawFlags { public int ApprovedById { get; set; } } public async Task ExportForPromotionAsync( int originEntityId, LoginDTO login, CancellationToken ct) { var json = await _WiMasterBLL.GetWiById(originEntityId, login, ct).ConfigureAwait(false); var dto = JsonConvert.DeserializeObject(json) ?? throw new InvalidOperationException($"WorkInstruction {originEntityId} not found."); var wiProcessCode = await _WiProcessCodeLookup.GetWiProcessCodeById(dto.WiProcessId, login, ct) .ConfigureAwait(false) ?? throw new InvalidOperationException( $"WorkInstruction {originEntityId} references WiProcessId {dto.WiProcessId}, which has no WiProcessCode — cannot promote."); var wiSubProcessCode = await _WiSubProcessCodeLookup.GetWiSubProcessCodeById(dto.WiSubProcessId, login, ct) .ConfigureAwait(false) ?? throw new InvalidOperationException( $"WorkInstruction {originEntityId} references WiSubProcessId {dto.WiSubProcessId}, which has no WiSubProcessCode — cannot promote."); var payload = new PortablePayload { WiCode = dto.WiCode, WiTitle = dto.WiTitle, WiProcessCode = wiProcessCode, WiSubProcessCode = wiSubProcessCode, ActivityType = (byte)dto.ActivityType, ActivitySubType = dto.ActivitySubType, WiVersion = dto.WiVersion, WiStatus = (byte)dto.WiStatus, DisplayContext = (byte)dto.DisplayContext, StepAdvanceMode = (byte)dto.StepAdvanceMode, StdCycleTimeMins = dto.StdCycleTimeMins, EffectiveDate = dto.EffectiveDate, ReviewDueDate = dto.ReviewDueDate, Tags = dto.Tags, WiDescription = dto.WiDescription, IsActive = dto.IsActive, Status = dto.Status, SortOrder = dto.SortOrder, Flags = new RawFlags { ApprovedById = dto.ApprovedById } }; return new PromotionBundleDTO { EntityTypeCode = EntityTypeCode, OriginEntityId = originEntityId, PayloadJson = System.Text.Json.JsonSerializer.Serialize(payload) }; } public Task ValidateForPromotionAsync( PromotionBundleDTO bundle, CancellationToken ct) { var payload = System.Text.Json.JsonSerializer.Deserialize(bundle.PayloadJson); if (payload is null) return Task.FromResult(PromotionValidationResultDTO.Block("WorkInstruction payload could not be parsed.")); var reasons = new System.Collections.Generic.List(); if (string.IsNullOrWhiteSpace(payload.WiCode)) reasons.Add("WorkInstruction.WiCode is required to promote."); if (string.IsNullOrWhiteSpace(payload.WiProcessCode)) reasons.Add("WorkInstruction.WiProcessId has no resolvable WiProcessCode — cannot promote."); if (string.IsNullOrWhiteSpace(payload.WiSubProcessCode)) reasons.Add("WorkInstruction.WiSubProcessId has no resolvable WiSubProcessCode — cannot promote."); if (payload.Flags.ApprovedById != 0) reasons.Add("WorkInstruction.ApprovedById is set — approver-identity remapping is not yet supported."); var result = reasons.Count == 0 ? PromotionValidationResultDTO.Ok() : new PromotionValidationResultDTO { IsPromotable = false, BlockingReasons = reasons }; return Task.FromResult(result); } public async Task ImportPromotedAsync( PromotionBundleDTO bundle, byte destinationSourceType, int? existingDestinationId, LoginDTO login, CancellationToken ct) { var payload = System.Text.Json.JsonSerializer.Deserialize(bundle.PayloadJson) ?? throw new InvalidOperationException("WorkInstruction promotion payload could not be parsed."); var wiProcessId = await _WiProcessCodeLookup.GetWiProcessIdByCode(payload.WiProcessCode, login, ct) .ConfigureAwait(false) ?? throw new InvalidOperationException( $"No WiProcess with WiProcessCode '{payload.WiProcessCode}' exists at this destination."); var wiSubProcessId = await _WiSubProcessCodeLookup.GetWiSubProcessIdByCode(payload.WiSubProcessCode, login, ct) .ConfigureAwait(false) ?? throw new InvalidOperationException( $"No WiSubProcess with WiSubProcessCode '{payload.WiSubProcessCode}' exists at this destination."); var resolvedId = existingDestinationId ?? await _WiHeaderCodeLookup.GetWiIdByCode(payload.WiCode, login, ct).ConfigureAwait(false) ?? 0; // Update: start from the destination's OWN current full aggregate (its real // Sections/Steps/Contents/SkillRequirements/CompetencyRequirements included) so those // child collections survive SaveWiFull's delete-and-rebuild update path untouched — see // the class header comment. Insert: nothing exists yet to preserve. WiSaveRequestDTO req; if (resolvedId != 0) { var existingJson = await _WiMasterBLL.GetWiFullDetail(resolvedId, login, ct).ConfigureAwait(false); req = JsonConvert.DeserializeObject(existingJson!) ?? throw new InvalidOperationException( $"WorkInstruction {resolvedId} could not be read at the destination for update."); } else { req = new WiSaveRequestDTO(); } req.Header.WiId = resolvedId; req.Header.WiCode = payload.WiCode; req.Header.WiTitle = payload.WiTitle; req.Header.WiProcessId = wiProcessId; req.Header.WiSubProcessId = wiSubProcessId; req.Header.ActivityType = (WiDAL.DTOs.ActivityType)payload.ActivityType; req.Header.ActivitySubType = payload.ActivitySubType; req.Header.WiVersion = payload.WiVersion; req.Header.WiStatus = (WiDAL.DTOs.WiStatus)payload.WiStatus; req.Header.DisplayContext = (WiDAL.DTOs.DisplayContext)payload.DisplayContext; req.Header.StepAdvanceMode = (WiDAL.DTOs.StepAdvanceMode)payload.StepAdvanceMode; req.Header.StdCycleTimeMins = payload.StdCycleTimeMins; req.Header.EffectiveDate = payload.EffectiveDate; req.Header.ReviewDueDate = payload.ReviewDueDate; req.Header.Tags = payload.Tags; req.Header.WiDescription = payload.WiDescription; req.Header.IsActive = payload.IsActive; req.Header.Status = payload.Status; req.Header.SortOrder = payload.SortOrder; req.Header.SourceType = destinationSourceType; req.Header.ApprovedById = 0; // never mapped — see class header comment req.Header.ApprovedOn = null; await _WiMasterBLL.SaveWiFull(req, login, ct).ConfigureAwait(false); return req.Header.WiId; // mutated in place by SaveWiFull when isNew } } }