using System; using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.ReportViewer; using FrameworkDAL.CustomCode.Promotion; using FrameworkDAL.DTO.ReportViewer; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using GB5Shared.Promotion; using Newtonsoft.Json; namespace FrameworkBLL.Promotion.Adapters { // ============================================================ // ReportViewPromotionAdapter — IPromotableEntity for MREPORTVIEW // ("REPORTVIEW"). Parent row only in this first pass — MREPORTVIEWFIELDS // (the per-column display settings) are deliberately NOT bundled yet: each // field row carries its own REPORTVSFIELDSID/DIMENSIONFIELDID FK // references, which have exactly the same cross-environment // ID-portability problem as ReportView's own FKs below, just one level // deeper. Solving that properly is a follow-up, not attempted here — // promoting the view's own settings without its field-level formatting is // still a useful, honest first slice, not a silent partial promise. // // ReportView has several raw ID references that do NOT survive a move // between two separate databases (AutoNumber-assigned IDs mean nothing // outside the database that assigned them): // - ReportId (mandatory) — resolved via MREPORT.ReportCode, the natural // key, both at export (Id -> Code) and import (Code -> local Id). // Import fails clearly if the destination has no matching Report — // referential completeness, not a silent broken FK. // - ReportHeaderId (optional) — resolved via MREPORTHEADER.ReportHeaderCode, // same natural-key pattern as ReportId, now that ReportHeaderPromotionAdapter // exists (REPORTHEADER is itself a promotable entity type). Null/absent if // the view has no header attached (ReportHeaderId at its -1/0 sentinel). // - TemplateId / SecondTemplateId / PageId / ApplicableMenuIds — still // optional, still conservatively BLOCKED if set to a real // (non-sentinel) value — no adapter exists yet for Template/Page/Menu // to resolve them via natural key the way ReportHeader now can. // Remapping these is a real, scoped follow-up, not attempted here. // - ClientIds — almost always tenant-specific data; treated as a // hardcoded-tenant-literal validation block, not merely an // ID-portability problem. // // IMPORTANT: these risky fields are still carried in the bundle payload // (under RawFlags) even though ImportPromotedAsync never maps them onto // the destination row. ValidateForPromotionAsync only sees the bundle // (never the live DTO) and runs at BOTH export time (fail fast) and // ingest time (never trust only the origin's copy) — if these fields // were trimmed away before validation ran, "block if ClientIds is set" // would never actually fire, since the dangerous value wouldn't be there // to inspect. Import simply ignores RawFlags — it exists for validation // to see, not for import to use. // // Note: TemplateId/PageId's DTO default (bare 0) doesn't match their own // DB column default (-1) -- a separate, pre-existing inconsistency this // adapter works around defensively by treating EITHER 0 or -1 as "unset", // rather than fixing that mismatch here (out of scope for Promotion). // ============================================================ public class ReportViewPromotionAdapter : IPromotableEntity { public string EntityTypeCode => "REPORTVIEW"; private readonly IReportViewBLL _ReportViewBLL; private readonly IReportCodeLookupDAL _ReportCodeLookup; private readonly IReportHeaderCodeLookupDAL _ReportHeaderCodeLookup; public ReportViewPromotionAdapter( IReportViewBLL reportViewBLL, IReportCodeLookupDAL reportCodeLookup, IReportHeaderCodeLookupDAL reportHeaderCodeLookup) { _ReportViewBLL = reportViewBLL; _ReportCodeLookup = reportCodeLookup; _ReportHeaderCodeLookup = reportHeaderCodeLookup; } private class RawFlags { public string? ClientIds { get; set; } public int TemplateId { get; set; } public int SecondTemplateId { get; set; } public int PageId { get; set; } public string? ApplicableMenuIds { get; set; } } private class PortablePayload { public string ReportCode { get; set; } = string.Empty; /// Null if the view has no header attached. public string? ReportHeaderCode { get; set; } public string ReportViewName { get; set; } = string.Empty; public byte ReportViewType { get; set; } public byte ReportViewIsSystemView { get; set; } public byte ReportViewIsDefaultView { get; set; } public byte ReportViewPageFormat { get; set; } public byte ReportViewPageOrientation { get; set; } public byte ReportViewColumnFreeze { get; set; } public string ReportViewPivotFormat { get; set; } = string.Empty; public byte ReportViewIsDataPivot { get; set; } public string OrderByFields { get; set; } = string.Empty; public string DynamicFooter { get; set; } = string.Empty; public string ReportTitle { get; set; } = string.Empty; public byte ReportViewIsHeaderRequired { get; set; } public byte IsVisible { get; set; } public string? ReportViewRemarks { get; set; } public string? Remarks { get; set; } public short SortOrder { get; set; } /// Seen only by ValidateForPromotionAsync — never mapped onto the /// destination row by ImportPromotedAsync. See the class header comment. public RawFlags Flags { get; set; } = new(); } public async Task ExportForPromotionAsync( int originEntityId, LoginDTO login, CancellationToken ct) { var json = await _ReportViewBLL.GetReportView(originEntityId, login, ct).ConfigureAwait(false); var dto = JsonConvert.DeserializeObject(json) ?? throw new InvalidOperationException($"ReportView {originEntityId} not found."); var reportCode = await _ReportCodeLookup.GetReportCodeById(dto.ReportId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"ReportView {originEntityId} references ReportId {dto.ReportId}, which has no ReportCode — cannot promote."); string? reportHeaderCode = null; if (dto.ReportHeaderId != 0 && dto.ReportHeaderId != -1) { reportHeaderCode = await _ReportHeaderCodeLookup.GetReportHeaderCodeById(dto.ReportHeaderId, login, ct) .ConfigureAwait(false) ?? throw new InvalidOperationException( $"ReportView {originEntityId} references ReportHeaderId {dto.ReportHeaderId}, " + "which has no ReportHeaderCode — cannot promote."); } var payload = new PortablePayload { ReportCode = reportCode, ReportHeaderCode = reportHeaderCode, ReportViewName = dto.ReportViewName, ReportViewType = dto.ReportViewType, ReportViewIsSystemView = dto.ReportViewIsSystemView, ReportViewIsDefaultView = dto.ReportViewIsDefaultView, ReportViewPageFormat = dto.ReportViewPageFormat, ReportViewPageOrientation = dto.ReportViewPageOrientation, ReportViewColumnFreeze = dto.ReportViewColumnFreeze, ReportViewPivotFormat = dto.ReportViewPivotFormat, ReportViewIsDataPivot = dto.ReportViewIsDataPivot, OrderByFields = dto.OrderByFields, DynamicFooter = dto.DynamicFooter, ReportTitle = dto.ReportTitle, ReportViewIsHeaderRequired = dto.ReportViewIsHeaderRequired, IsVisible = dto.IsVisible, ReportViewRemarks = dto.ReportViewRemarks, Remarks = dto.Remarks, SortOrder = dto.SortOrder, Flags = new RawFlags { ClientIds = dto.ClientIds, TemplateId = dto.TemplateId, SecondTemplateId = dto.SecondTemplateId, PageId = dto.PageId, ApplicableMenuIds = dto.ApplicableMenuIds } }; return new PromotionBundleDTO { EntityTypeCode = EntityTypeCode, OriginEntityId = originEntityId, PayloadJson = System.Text.Json.JsonSerializer.Serialize(payload), ChildPayloadsJson = new Dictionary() // Fields deliberately not bundled yet }; } 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("ReportView payload could not be parsed.")); var reasons = new List(); var flags = payload.Flags; var clientIds = flags.ClientIds?.Trim(); if (!string.IsNullOrEmpty(clientIds) && clientIds != ",-1") reasons.Add("ReportView.ClientIds references specific client(s) — not promotable."); if (flags.TemplateId != 0 && flags.TemplateId != -1) reasons.Add("ReportView.TemplateId is set — template remapping is not yet supported."); if (flags.SecondTemplateId != 0 && flags.SecondTemplateId != -1) reasons.Add("ReportView.SecondTemplateId is set — template remapping is not yet supported."); if (flags.PageId != 0 && flags.PageId != -1) reasons.Add("ReportView.PageId is set — page remapping is not yet supported."); if (!string.IsNullOrWhiteSpace(flags.ApplicableMenuIds)) reasons.Add("ReportView.ApplicableMenuIds is set — menu 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("ReportView promotion payload could not be parsed."); var 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 reportHeaderId = -1; // sentinel: no header attached if (!string.IsNullOrWhiteSpace(payload.ReportHeaderCode)) { reportHeaderId = await _ReportHeaderCodeLookup.GetReportHeaderIdByCode(payload.ReportHeaderCode, login, ct) .ConfigureAwait(false) ?? throw new InvalidOperationException( $"No ReportHeader with ReportHeaderCode '{payload.ReportHeaderCode}' exists at this " + "destination — promote the ReportHeader itself first."); } // payload.Flags is deliberately NOT read here — see the class header comment. // TemplateId/PageId are explicitly stamped to their real "unset" sentinel (-1, which // has a genuine dummy MFILE row) rather than left at ReportViewDTO's own bare-0 // default — MREPORTVIEW.TEMPLATEID/PAGEID are NOT NULL with FKs to MFILE, which has // no FILEID=0 row, so leaving them at 0 throws a real FK violation on every import. // Found live 2026-08-21 verifying the first-ever real Promotion Import execution. var dto = new ReportViewDTO { ReportViewId = existingDestinationId ?? 0, ReportId = reportId, ReportHeaderId = reportHeaderId, TemplateId = -1, PageId = -1, ReportViewName = payload.ReportViewName, ReportViewType = payload.ReportViewType, ReportViewIsSystemView = payload.ReportViewIsSystemView, ReportViewIsDefaultView = payload.ReportViewIsDefaultView, ReportViewPageFormat = payload.ReportViewPageFormat, ReportViewPageOrientation = payload.ReportViewPageOrientation, ReportViewColumnFreeze = payload.ReportViewColumnFreeze, ReportViewPivotFormat = payload.ReportViewPivotFormat, ReportViewIsDataPivot = payload.ReportViewIsDataPivot, OrderByFields = payload.OrderByFields, DynamicFooter = payload.DynamicFooter, ReportTitle = payload.ReportTitle, ReportViewIsHeaderRequired = payload.ReportViewIsHeaderRequired, IsVisible = payload.IsVisible, ReportViewRemarks = payload.ReportViewRemarks, Remarks = payload.Remarks, SortOrder = payload.SortOrder, ReportViewSourceType = destinationSourceType }; await _ReportViewBLL.SaveReportView(dto, login, ct).ConfigureAwait(false); return dto.ReportViewId; // mutated in place by SaveReportView when isNew } } }