using System; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.Page; using FrameworkDAL.CustomCode.Promotion; using FrameworkDAL.DTO.Page; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using GB5Shared.Promotion; using Newtonsoft.Json; namespace FrameworkBLL.Promotion.Adapters { // ============================================================ // PagePromotionAdapter — IPromotableEntity for MPAGE ("PAGE"). Parent row // only — MPAGE has no child/detail rows of its own (Dashboard->Page // placement lives on the Dashboard side, in MDASHBOARDPAGE, not here). // // PageCode is the natural key, resolved via the new IPageCodeLookupDAL — // this is also what unblocks DashboardPromotionAdapter's own PageId // references (MDASHBOARDPAGE rows) and ReportView's own PageId (still // blocked there pending this adapter's registration). // // ModuleId/ModuleCode reuses the existing IModuleCodeLookupDAL (module // codes are static/shared across installs, low risk, same reasoning as // ReportView/DrillDown's own Module reference). // // DefaultCriteriaConfigId and ImageId are raw AutoNumber-ID references // with no natural-key lookup built yet — conservatively BLOCKED if set to // a real (non-sentinel) value, same "carry it in Flags for validation to // see, never map it at import" pattern as ReportView's own risky fields. // ============================================================ public class PagePromotionAdapter : IPromotableEntity { public string EntityTypeCode => "PAGE"; private readonly IPageBLL _PageBLL; private readonly IPageCodeLookupDAL _PageCodeLookup; private readonly IModuleCodeLookupDAL _ModuleCodeLookup; public PagePromotionAdapter( IPageBLL pageBLL, IPageCodeLookupDAL pageCodeLookup, IModuleCodeLookupDAL moduleCodeLookup) { _PageBLL = pageBLL; _PageCodeLookup = pageCodeLookup; _ModuleCodeLookup = moduleCodeLookup; } private class RawFlags { public int DefaultCriteriaConfigId { get; set; } public int ImageId { get; set; } } private class PortablePayload { public string PageCode { get; set; } = string.Empty; public string PageName { get; set; } = string.Empty; public string? PageRemarks { get; set; } public string? PageLayOut { get; set; } public byte PageType { get; set; } public byte OverrideDashboardCriteria { get; set; } public string? ModuleCode { get; set; } public string? ImageLinkURL { get; set; } public short PageSortOrder { get; set; } /// Seen only by ValidateForPromotionAsync — ImportPromotedAsync never maps /// these onto the destination row. See the class header comment. public RawFlags Flags { get; set; } = new(); } public async Task ExportForPromotionAsync( int originEntityId, LoginDTO login, CancellationToken ct) { var json = await _PageBLL.GetPage(originEntityId, login).ConfigureAwait(false); var dto = JsonConvert.DeserializeObject(json) ?? throw new InvalidOperationException($"Page {originEntityId} not found."); string? moduleCode = null; if (dto.ModuleId != 0 && dto.ModuleId != -1) { moduleCode = await _ModuleCodeLookup.GetModuleCodeById(dto.ModuleId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"Page {originEntityId} references ModuleId {dto.ModuleId}, which has no ModuleCode — cannot promote."); } var payload = new PortablePayload { PageCode = dto.PageCode, PageName = dto.PageName, PageRemarks = dto.PageRemarks, PageLayOut = dto.PageLayOut, PageType = dto.PageType, OverrideDashboardCriteria = dto.OverrideDashboardCriteria, ModuleCode = moduleCode, ImageLinkURL = dto.ImageLinkURL, PageSortOrder = dto.PageSortOrder, Flags = new RawFlags { DefaultCriteriaConfigId = dto.DefaultCriteriaConfigId, ImageId = dto.ImageId } }; 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("Page payload could not be parsed.")); var reasons = new System.Collections.Generic.List(); var flags = payload.Flags; if (flags.DefaultCriteriaConfigId != 0 && flags.DefaultCriteriaConfigId != -1) reasons.Add("Page.DefaultCriteriaConfigId is set — criteria-config remapping is not yet supported."); if (flags.ImageId != 0 && flags.ImageId != -1) reasons.Add("Page.ImageId is set — image 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("Page promotion payload could not be parsed."); var moduleId = -1; if (!string.IsNullOrWhiteSpace(payload.ModuleCode)) { moduleId = await _ModuleCodeLookup.GetModuleIdByCode(payload.ModuleCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"No Module with ModuleCode '{payload.ModuleCode}' exists at this destination."); } // If this is an update to a previously-promoted page, existingDestinationId already // identifies it directly. Otherwise, check whether a page with this code already exists // at the destination (created independently, not via a prior promotion) so Import // updates it rather than violating PageCode's own unique constraint with a duplicate. var resolvedId = existingDestinationId ?? await _PageCodeLookup.GetPageIdByCode(payload.PageCode, login, ct).ConfigureAwait(false) ?? 0; var dto = new PageDTO { PageId = resolvedId, PageCode = payload.PageCode, PageName = payload.PageName, PageRemarks = payload.PageRemarks, PageLayOut = payload.PageLayOut, PageType = payload.PageType, OverrideDashboardCriteria = payload.OverrideDashboardCriteria, ModuleId = moduleId, ImageId = -1, // never mapped — see class header comment ImageLinkURL = payload.ImageLinkURL, PageSortOrder = payload.PageSortOrder, PageSourceType = destinationSourceType, DefaultCriteriaConfigId = -1 // never mapped — see class header comment }; await _PageBLL.SavePage(dto, login).ConfigureAwait(false); return dto.PageId; // mutated in place by SavePage when isNew } } }