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
}
}
}