using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.ReportHeaderMgmt; using FrameworkDAL.CustomCode.Promotion; using FrameworkDAL.DTO.ReportHeader; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using GB5Shared.Promotion; using Newtonsoft.Json; namespace FrameworkBLL.Promotion.Adapters { // ============================================================ // ReportHeaderPromotionAdapter — IPromotableEntity for MREPORTHEADER // ("REPORTHEADER"). Unlike ReportView/DrillDown, this entity's own detail // rows (MREPORTHEADERDETAIL — grid-cell layout: row/column spans, font, // color, expression) are all plain values with no cross-environment ID // references, so the full parent+children bundle travels together with // no blocked fields at all. // // ReportHeaderCode is the natural key (MREPORTHEADER's own unique // constraint is (ReportHeaderCode, ClientId), not ReportHeaderCode alone) // — resolved at both export and import, same pattern as ReportView's // ReportCode. A header explicitly scoped to a specific ClientId (not the // -1 "not tenant-specific" sentinel) is blocked from promotion entirely, // same tenant-literal reasoning as ReportView's ClientIds check. At // import, ClientId is always forced to -1 regardless of the origin's own // value — a promoted header is shared/framework-level by definition, never // bound to one specific client at the destination either. // // Enables ReportViewPromotionAdapter to resolve ReportHeaderId going // forward (previously unconditionally blocked) once this entity type is // registered and promotable — see ReportViewPromotionAdapter's own updated // comment. // // Note: like ReportViewBLL, ReportHeaderBLL.SaveReportHeader never sets // dto.ReportHeaderSourceType server-side either (confirmed by reading it) // — this adapter sets it explicitly before calling Save, same as the // ReportView adapter does, but the underlying gap in ReportHeaderBLL is // real and separate, flagged not fixed (out of scope for Promotion). // ============================================================ public class ReportHeaderPromotionAdapter : IPromotableEntity { public string EntityTypeCode => "REPORTHEADER"; private readonly IReportHeaderBLL _ReportHeaderBLL; private readonly IReportHeaderCodeLookupDAL _ReportHeaderCodeLookup; public ReportHeaderPromotionAdapter( IReportHeaderBLL reportHeaderBLL, IReportHeaderCodeLookupDAL reportHeaderCodeLookup) { _ReportHeaderBLL = reportHeaderBLL; _ReportHeaderCodeLookup = reportHeaderCodeLookup; } private class DetailPayload { public short SlNo { get; set; } public byte FromRowNumber { get; set; } public byte ToRowNumber { get; set; } public byte RowHeight { get; set; } public byte FromColumnNumber { get; set; } public byte ToColumnNumber { get; set; } public byte CellWidth { get; set; } public string? Expression { get; set; } public byte Alignment { get; set; } public string? Font { get; set; } public byte FontSize { get; set; } public string? Color { get; set; } public byte Style { get; set; } public byte IsEmptySuppress { get; set; } } private class RawFlags { public int ClientId { get; set; } } private class PortablePayload { public string ReportHeaderCode { get; set; } = string.Empty; public string ReportHeaderName { get; set; } = string.Empty; public string? Particulars { get; set; } public string? Remarks { get; set; } public short SortOrder { get; set; } public List Details { get; set; } = new(); /// Seen only by ValidateForPromotionAsync — ImportPromotedAsync always forces /// ClientId to -1 at the destination regardless of this value. public RawFlags Flags { get; set; } = new(); } public async Task ExportForPromotionAsync( int originEntityId, LoginDTO login, CancellationToken ct) { var json = await _ReportHeaderBLL.GetReportHeader(originEntityId, login, ct).ConfigureAwait(false); var dto = JsonConvert.DeserializeObject(json) ?? throw new InvalidOperationException($"ReportHeader {originEntityId} not found."); var payload = new PortablePayload { ReportHeaderCode = dto.ReportHeaderCode, ReportHeaderName = dto.ReportHeaderName, Particulars = dto.ReportHeaderParticulars, Remarks = dto.ReportHeaderRemarks, SortOrder = dto.ReportHeaderSortOrder, Details = (dto.ReportHeaderDetailArray ?? new List()).Select(d => new DetailPayload { SlNo = d.ReportHeaderDetailSlNo, FromRowNumber = d.ReportHeaderDetailFromRowNumber, ToRowNumber = d.ReportHeaderDetailToRowNumber, RowHeight = d.ReportHeaderDetailRowHeight, FromColumnNumber = d.ReportHeaderDetailFromColumnNumber, ToColumnNumber = d.ReportHeaderDetailToColumnNumber, CellWidth = d.ReportHeaderDetailCellWidth, Expression = d.ReportHeaderDetailExpression, Alignment = d.ReportHeaderDetailAlignment, Font = d.ReportHeaderDetailFont, FontSize = d.ReportHeaderDetailFontSize, Color = d.ReportHeaderDetailColor, Style = d.ReportHeaderDetailStyle, IsEmptySuppress = d.ReportHeaderDetailIsEmptySuppress }).ToList(), Flags = new RawFlags { ClientId = dto.ReportHeaderBaseClinetId } }; 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("ReportHeader payload could not be parsed.")); var result = payload.Flags.ClientId != -1 ? PromotionValidationResultDTO.Block("ReportHeader is scoped to a specific ClientId — not promotable.") : PromotionValidationResultDTO.Ok(); 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("ReportHeader promotion payload could not be parsed."); // If this is an update to a previously-promoted header, existingDestinationId already // identifies it directly — no code lookup needed. Otherwise, check whether a header with // this code already exists at the destination (created independently, not via a prior // promotion) so Import updates it rather than violating the (Code, ClientId) unique // constraint with a duplicate insert. var resolvedId = existingDestinationId ?? await _ReportHeaderCodeLookup.GetReportHeaderIdByCode(payload.ReportHeaderCode, login, ct).ConfigureAwait(false) ?? 0; var dto = new ReportHeaderDTO { ReportHeaderId = resolvedId, ReportHeaderCode = payload.ReportHeaderCode, ReportHeaderName = payload.ReportHeaderName, ReportHeaderParticulars = payload.Particulars, ReportHeaderRemarks = payload.Remarks, ReportHeaderSortOrder = payload.SortOrder, ReportHeaderSourceType = destinationSourceType, ReportHeaderBaseClinetId = -1, // always forced — see class header comment ReportHeaderDetailArray = payload.Details.Select(d => new ReportHeaderDetailDTO { ReportHeaderDetailSlNo = d.SlNo, ReportHeaderDetailFromRowNumber = d.FromRowNumber, ReportHeaderDetailToRowNumber = d.ToRowNumber, ReportHeaderDetailRowHeight = d.RowHeight, ReportHeaderDetailFromColumnNumber = d.FromColumnNumber, ReportHeaderDetailToColumnNumber = d.ToColumnNumber, ReportHeaderDetailCellWidth = d.CellWidth, ReportHeaderDetailExpression = d.Expression, ReportHeaderDetailAlignment = d.Alignment, ReportHeaderDetailFont = d.Font, ReportHeaderDetailFontSize = d.FontSize, ReportHeaderDetailColor = d.Color, ReportHeaderDetailStyle = d.Style, ReportHeaderDetailIsEmptySuppress = d.IsEmptySuppress }).ToList() }; await _ReportHeaderBLL.SaveReportHeader(dto, login, ct).ConfigureAwait(false); return dto.ReportHeaderId; // mutated in place by SaveReportHeader when isNew } } }