using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.PortLet; using FrameworkDAL.CustomCode.Promotion; using FrameworkDAL.DTO.Portlet; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using GB5Shared.Promotion; using Newtonsoft.Json; namespace FrameworkBLL.Promotion.Adapters { // ============================================================ // PortletPromotionAdapter — IPromotableEntity for MPORTLET ("PORTLET"). // Parent row only — no child rows of its own. // // Resolved by natural key: PortletTypeId (via PortletTypePromotionAdapter's // own IPortletTypeCodeLookupDAL, now that PortletType is itself // promotable), ModuleId/MenuId/FormLinkMenuId (existing Module/Menu code // lookups — Menu is reused twice, for the two independent menu-shaped // references), and ReportId (existing IReportCodeLookupDAL, same as // ReportView — MPORTLET has no ReportCode column of its own, only the // raw Id, so this still needs the same Id<->Code round trip ReportView // already does). // // Conservatively BLOCKED (confirmed via direct schema search — only // BIViewId has a real FK constraint on MPORTLET, but every one of these // is a genuine cross-environment reference with no natural-key lookup // built yet, or in ProgramId's case, no natural key possible at all): // - ProgramId — MPROGRAM has no Code column whatsoever (confirmed); // this is a permanent block, not a "not yet built" gap. // - BIViewId/KPIId/KPIListId/PortletKPIOUId — depend on BIView/KPI // becoming promotable entities first (both deferred as // high-complexity — see the plan). Populated only for BI/KPI-typed // portlets (discriminated by PortletTypeName, not a separate flag). // - WebServiceSettingId, PortletSettingsId, DefaultCriteriaConfigId, // FileId, ReportFormatId — raw AutoNumber references, no lookup // DAL exists for any of them yet. // These are still carried in the bundle payload (under Flags) even // though ImportPromotedAsync never maps them — ValidateForPromotionAsync // needs to see the real value to decide whether to block; trimming them // first would make the block never actually fire. Same pattern as // ReportViewPromotionAdapter's own RawFlags. // ============================================================ public class PortletPromotionAdapter : IPromotableEntity { public string EntityTypeCode => "PORTLET"; private readonly IPortletBLL _PortletBLL; private readonly IReportCodeLookupDAL _ReportCodeLookup; private readonly IModuleCodeLookupDAL _ModuleCodeLookup; private readonly IMenuCodeLookupDAL _MenuCodeLookup; private readonly IPortletTypeCodeLookupDAL _PortletTypeCodeLookup; private readonly IPortletCodeLookupDAL _PortletCodeLookup; public PortletPromotionAdapter( IPortletBLL portletBLL, IReportCodeLookupDAL reportCodeLookup, IModuleCodeLookupDAL moduleCodeLookup, IMenuCodeLookupDAL menuCodeLookup, IPortletTypeCodeLookupDAL portletTypeCodeLookup, IPortletCodeLookupDAL portletCodeLookup) { _PortletBLL = portletBLL; _ReportCodeLookup = reportCodeLookup; _ModuleCodeLookup = moduleCodeLookup; _MenuCodeLookup = menuCodeLookup; _PortletTypeCodeLookup = portletTypeCodeLookup; _PortletCodeLookup = portletCodeLookup; } private class RawFlags { public int ProgramId { get; set; } public int? BIViewId { get; set; } public int? KPIId { get; set; } public int? KPIListId { get; set; } public int? PortletKPIOUId { get; set; } public int WebServiceSettingId { get; set; } public int PortletSettingsId { get; set; } public int DefaultCriteriaConfigId { get; set; } public int FileId { get; set; } public int ReportFormatId { get; set; } } private class PortablePayload { public string PortletCode { get; set; } = string.Empty; public string PortletName { get; set; } = string.Empty; public string? PortletTypeCode { get; set; } public string? ModuleCode { get; set; } public string? ReportCode { get; set; } public string? MenuCode { get; set; } public string? FormLinkMenuCode { get; set; } public string? PortletSection { get; set; } public string? PortletUrl { get; set; } public byte PortletIsChangeable { get; set; } public byte PortletIsMaximize { get; set; } public byte PortletIsResize { get; set; } public byte PortletIsPrint { get; set; } public byte PortletIsExport { get; set; } public byte PortletIsSettings { get; set; } public byte PortletIsChangeSetting { get; set; } public byte PortletChartType { get; set; } public string? PortletChartXAxis { get; set; } public string? PortletChartYAxis { get; set; } public string? PortletChartValue { get; set; } public byte PortletIsPagenationRequired { get; set; } public byte PortletNumberOfRecords { get; set; } public Int16 PortletHeight { get; set; } public Int16 PortletWidth { get; set; } public string? PortletXaxisDateFormat { get; set; } public string? PortletYaxisDateFormat { get; set; } public Int16 PortletSortOrder { 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 _PortletBLL.GetPortlet(originEntityId, login).ConfigureAwait(false); var dto = JsonConvert.DeserializeObject(json) ?? throw new InvalidOperationException($"Portlet {originEntityId} not found."); string? portletTypeCode = null; if (dto.PortletTypeId != 0 && dto.PortletTypeId != -1) portletTypeCode = await _PortletTypeCodeLookup.GetPortletTypeCodeById(dto.PortletTypeId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"Portlet {originEntityId} references PortletTypeId {dto.PortletTypeId}, which has no PortletTypeCode — cannot promote."); string? moduleCode = null; if (dto.ModuleId != 0 && dto.ModuleId != -1) moduleCode = await _ModuleCodeLookup.GetModuleCodeById(dto.ModuleId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"Portlet {originEntityId} references ModuleId {dto.ModuleId}, which has no ModuleCode — cannot promote."); string? reportCode = null; if (dto.ReportId != 0 && dto.ReportId != -1) reportCode = await _ReportCodeLookup.GetReportCodeById(dto.ReportId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"Portlet {originEntityId} references ReportId {dto.ReportId}, which has no ReportCode — cannot promote."); string? menuCode = null; if (dto.MenuId != 0 && dto.MenuId != -1) menuCode = await _MenuCodeLookup.GetMenuCodeById(dto.MenuId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"Portlet {originEntityId} references MenuId {dto.MenuId}, which has no MenuCode — cannot promote."); string? formLinkMenuCode = null; if (dto.FormLinkMenuId != 0 && dto.FormLinkMenuId != -1) formLinkMenuCode = await _MenuCodeLookup.GetMenuCodeById(dto.FormLinkMenuId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"Portlet {originEntityId} references FormLinkMenuId {dto.FormLinkMenuId}, which has no MenuCode — cannot promote."); var payload = new PortablePayload { PortletCode = dto.PortletCode, PortletName = dto.PortletName, PortletTypeCode = portletTypeCode, ModuleCode = moduleCode, ReportCode = reportCode, MenuCode = menuCode, FormLinkMenuCode = formLinkMenuCode, PortletSection = dto.PortletSection, PortletUrl = dto.PortletUrl, PortletIsChangeable = dto.PortletIsChangeable, PortletIsMaximize = dto.PortletIsMaximize, PortletIsResize = dto.PortletIsResize, PortletIsPrint = dto.PortletIsPrint, PortletIsExport = dto.PortletIsExport, PortletIsSettings = dto.PortletIsSettings, PortletIsChangeSetting = dto.PortletIsChangeSetting, PortletChartType = dto.PortletChartType, PortletChartXAxis = dto.PortletChartXAxis, PortletChartYAxis = dto.PortletChartYAxis, PortletChartValue = dto.PortletChartValue, PortletIsPagenationRequired = dto.PortletIsPagenationRequired, PortletNumberOfRecords = dto.PortletNumberOfRecords, PortletHeight = dto.PortletHeight, PortletWidth = dto.PortletWidth, PortletXaxisDateFormat = dto.PortletXaxisDateFormat, PortletYaxisDateFormat = dto.PortletYaxisDateFormat, PortletSortOrder = dto.PortletSortOrder, Flags = new RawFlags { ProgramId = dto.ProgramId, BIViewId = dto.BIViewId, KPIId = dto.KPIId, KPIListId = dto.KPIListId, PortletKPIOUId = dto.PortletKPIOUId, WebServiceSettingId = dto.WebServiceSettingId, PortletSettingsId = dto.PortletSettingsId, DefaultCriteriaConfigId = dto.DefaultCriteriaConfigId, FileId = dto.FileId, ReportFormatId = dto.ReportFormatId } }; 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("Portlet payload could not be parsed.")); var reasons = new List(); var flags = payload.Flags; if (flags.ProgramId != 0 && flags.ProgramId != -1) reasons.Add("Portlet.ProgramId is set — MPROGRAM has no natural key, so this can never be remapped."); if (flags.BIViewId is not null and not 0 and not -1) reasons.Add("Portlet.BIViewId is set — BIView is not yet a promotable entity."); if (flags.KPIId is not null and not 0 and not -1) reasons.Add("Portlet.KPIId is set — KPI is not yet a promotable entity."); if (flags.KPIListId is not null and not 0 and not -1) reasons.Add("Portlet.KPIListId is set — KPI List is not yet a promotable entity."); if (flags.PortletKPIOUId is not null and not 0 and not -1) reasons.Add("Portlet.PortletKPIOUId is set — org-unit remapping is not yet supported."); if (flags.WebServiceSettingId != 0 && flags.WebServiceSettingId != -1) reasons.Add("Portlet.WebServiceSettingId is set — web-service-setting remapping is not yet supported."); if (flags.PortletSettingsId != 0 && flags.PortletSettingsId != -1) reasons.Add("Portlet.PortletSettingsId is set — settings remapping is not yet supported."); if (flags.DefaultCriteriaConfigId != 0 && flags.DefaultCriteriaConfigId != -1) reasons.Add("Portlet.DefaultCriteriaConfigId is set — criteria-config remapping is not yet supported."); if (flags.FileId != 0 && flags.FileId != -1) reasons.Add("Portlet.FileId is set — file remapping is not yet supported."); if (flags.ReportFormatId != 0 && flags.ReportFormatId != -1) reasons.Add("Portlet.ReportFormatId is set — report-format 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("Portlet promotion payload could not be parsed."); var portletTypeId = -1; if (!string.IsNullOrWhiteSpace(payload.PortletTypeCode)) portletTypeId = await _PortletTypeCodeLookup.GetPortletTypeIdByCode(payload.PortletTypeCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"No PortletType with PortletTypeCode '{payload.PortletTypeCode}' exists at this destination — promote the PortletType itself first."); 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."); var reportId = -1; if (!string.IsNullOrWhiteSpace(payload.ReportCode)) reportId = await _ReportCodeLookup.GetReportIdByCode(payload.ReportCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"No Report with ReportCode '{payload.ReportCode}' exists at this destination — promote the Report itself first."); var menuId = -1; if (!string.IsNullOrWhiteSpace(payload.MenuCode)) menuId = await _MenuCodeLookup.GetMenuIdByCode(payload.MenuCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"No Menu with MenuCode '{payload.MenuCode}' exists at this destination."); var formLinkMenuId = -1; if (!string.IsNullOrWhiteSpace(payload.FormLinkMenuCode)) formLinkMenuId = await _MenuCodeLookup.GetMenuIdByCode(payload.FormLinkMenuCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"No Menu with MenuCode '{payload.FormLinkMenuCode}' exists at this destination."); // If this is an update to a previously-promoted portlet, existingDestinationId already // identifies it directly. Otherwise, check whether a portlet with this code already // exists at the destination (created independently, not via a prior promotion) so // Import updates it rather than violating PortletCode's own unique constraint // (UKMPORTLET_CODE, confirmed via schema) with a duplicate insert. var resolvedId = existingDestinationId ?? await _PortletCodeLookup.GetPortletIdByCode(payload.PortletCode, login, ct).ConfigureAwait(false) ?? 0; var dto = new PortletDTO { PortletId = resolvedId, PortletCode = payload.PortletCode, PortletName = payload.PortletName, PortletTypeId = portletTypeId, ModuleId = moduleId, ReportId = reportId, MenuId = menuId, FormLinkMenuId = formLinkMenuId, PortletSection = payload.PortletSection, PortletUrl = payload.PortletUrl, PortletIsChangeable = payload.PortletIsChangeable, PortletIsMaximize = payload.PortletIsMaximize, PortletIsResize = payload.PortletIsResize, PortletIsPrint = payload.PortletIsPrint, PortletIsExport = payload.PortletIsExport, PortletIsSettings = payload.PortletIsSettings, PortletIsChangeSetting = payload.PortletIsChangeSetting, PortletChartType = payload.PortletChartType, PortletChartXAxis = payload.PortletChartXAxis, PortletChartYAxis = payload.PortletChartYAxis, PortletChartValue = payload.PortletChartValue, PortletIsPagenationRequired = payload.PortletIsPagenationRequired, PortletNumberOfRecords = payload.PortletNumberOfRecords, PortletHeight = payload.PortletHeight, PortletWidth = payload.PortletWidth, PortletXaxisDateFormat = payload.PortletXaxisDateFormat, PortletYaxisDateFormat = payload.PortletYaxisDateFormat, PortletSortOrder = payload.PortletSortOrder, PortletSourceType = destinationSourceType, // Never mapped — see class header comment. ProgramId = -1, BIViewId = null, KPIId = null, KPIListId = null, PortletKPIOUId = null, WebServiceSettingId = -1, PortletSettingsId = -1, DefaultCriteriaConfigId = -1, FileId = -1, ReportFormatId = -1 }; await _PortletBLL.SavePortlet(dto, login).ConfigureAwait(false); return dto.PortletId; // mutated in place by SavePortlet when isNew } } }