using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.DrillDown; using FrameworkDAL.CustomCode.Promotion; using FrameworkDAL.DTO.DrillDown; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using GB5Shared.Promotion; using Newtonsoft.Json; namespace FrameworkBLL.Promotion.Adapters { // ============================================================ // DrillDownPromotionAdapter — IPromotableEntity for MDRILLDOWN // ("DRILLDOWN"). No SourceType column/concept exists on this entity at // all (confirmed — no such property anywhere on DrillDownDTO), so // destinationSourceType is accepted (interface contract) but simply // unused here; there is no tier to stamp. // // DrillDown is a discriminated reference: DrillDownFromType/DrillDownToType // determine which of {Menu, Portlet, Report} the from/to side actually is, // and only the matching *Id field is meaningful for a given row — the // others are left at their default. This adapter supports Report-anchored // drill-downs (the common report-to-report case) by resolving // FromReportCode/ToReportCode via the same IReportCodeLookupDAL the // ReportView adapter uses, and Module via IModuleCodeLookupDAL (module // codes are static/shared across installs, low risk). Menu/Portlet/WebForm // -anchored drill-downs are NOT yet supported — blocked conservatively if // any of those raw ID fields is set, since GB5's real AutoNumber-minted // IDs are large negative numbers (never 0), so 0 is a safe "not set" // sentinel for these specific fields — this assumption is stated // explicitly here rather than silently relied upon; verify against live // schema before trusting it for a table this adapter hasn't touched // before. // // Detail rows (MDRILLDOWNDETAIL) travel with the parent. Their own // AttributeId uses -1 (not 0) as its documented "unset" sentinel — see // DrillDownDetailDTO's own `private int attributeid = -1;` default — a // set AttributeId blocks promotion for the same no-natural-key reason. // ============================================================ public class DrillDownPromotionAdapter : IPromotableEntity { public string EntityTypeCode => "DRILLDOWN"; private readonly IDrillDownBLL _DrillDownBLL; private readonly IReportCodeLookupDAL _ReportCodeLookup; private readonly IModuleCodeLookupDAL _ModuleCodeLookup; public DrillDownPromotionAdapter( IDrillDownBLL drillDownBLL, IReportCodeLookupDAL reportCodeLookup, IModuleCodeLookupDAL moduleCodeLookup) { _DrillDownBLL = drillDownBLL; _ReportCodeLookup = reportCodeLookup; _ModuleCodeLookup = moduleCodeLookup; } private class DetailPayload { public short SlNo { get; set; } public byte MapType { get; set; } public int AttributeId { get; set; } // validated at -1 sentinel; never mapped if unsupported public byte AssignType { get; set; } public string? SourceField { get; set; } public string? FilterValue { get; set; } public byte OperationType { get; set; } public byte JoinType { get; set; } } private class RawFlags { public int FromMenuId { get; set; } public int ToMenuId { get; set; } public int FromPortletId { get; set; } public int ToPortletId { get; set; } public int ToWebFormId { get; set; } public int FromMenuReportWebServiceId { get; set; } public int ToMenuReportWebServiceId { get; set; } public bool AnyDetailHasAttributeId { get; set; } } private class PortablePayload { public string DrillDownCode { get; set; } = string.Empty; public string DrillDownDescription { get; set; } = string.Empty; public byte DrillDownType { get; set; } public byte DrillDownFromType { get; set; } public byte DrillDownToType { get; set; } public string? FromReportCode { get; set; } public string? ToReportCode { get; set; } public string? ModuleCode { get; set; } public string? DrillDownBIZTransactionTypeField { get; set; } public short SortOrder { get; set; } public List Details { get; set; } = new(); /// Seen only by ValidateForPromotionAsync — never mapped onto the /// destination row by ImportPromotedAsync. public RawFlags Flags { get; set; } = new(); } public async Task ExportForPromotionAsync( int originEntityId, LoginDTO login, CancellationToken ct) { var json = await _DrillDownBLL.GetDrillDown(originEntityId, login).ConfigureAwait(false); var dto = JsonConvert.DeserializeObject(json) ?? throw new InvalidOperationException($"DrillDown {originEntityId} not found."); var payload = new PortablePayload { DrillDownCode = dto.DrillDownCode, DrillDownDescription = dto.DrillDownDescription, DrillDownType = dto.DrillDownType, DrillDownFromType = dto.DrillDownFromType, DrillDownToType = dto.DrillDownToType, FromReportCode = dto.FromReportCode, ToReportCode = dto.ToReportCode, ModuleCode = dto.ModuleCode, DrillDownBIZTransactionTypeField = dto.DrillDownBIZTransactionTypeField, SortOrder = dto.DrillDownSortOrder, Details = (dto.DrillDownDetailArray ?? new List()).Select(d => new DetailPayload { SlNo = d.DrillDownDetailSlNo, MapType = d.DrillDownDetailMapType, AttributeId = d.AttributeId, AssignType = d.DrillDownDetailAssignType, SourceField = d.DrillDownDetailSourceField, FilterValue = d.DrillDownDetailFilterValue, OperationType = d.DrillDownDetailOperationType, JoinType = d.DrillDownDetailJoinType }).ToList(), Flags = new RawFlags { FromMenuId = dto.FromMenuId, ToMenuId = dto.ToMenuId, FromPortletId = dto.FromPortletId, ToPortletId = dto.ToPortletId, ToWebFormId = dto.ToWebFormId, FromMenuReportWebServiceId = dto.FromMenuReportWebServiceId, ToMenuReportWebServiceId = dto.ToMenuReportWebServiceId, AnyDetailHasAttributeId = (dto.DrillDownDetailArray ?? new List()) .Any(d => d.AttributeId != -1) } }; 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("DrillDown payload could not be parsed.")); var reasons = new List(); var f = payload.Flags; if (f.FromMenuId != 0) reasons.Add("DrillDown.FromMenuId is set — menu-anchored drill-downs are not yet supported."); if (f.ToMenuId != 0) reasons.Add("DrillDown.ToMenuId is set — menu-anchored drill-downs are not yet supported."); if (f.FromPortletId != 0) reasons.Add("DrillDown.FromPortletId is set — portlet-anchored drill-downs are not yet supported."); if (f.ToPortletId != 0) reasons.Add("DrillDown.ToPortletId is set — portlet-anchored drill-downs are not yet supported."); if (f.ToWebFormId != 0) reasons.Add("DrillDown.ToWebFormId is set — webform targets are not yet supported."); if (f.FromMenuReportWebServiceId != 0) reasons.Add("DrillDown.FromMenuReportWebServiceId is set — not yet supported."); if (f.ToMenuReportWebServiceId != 0) reasons.Add("DrillDown.ToMenuReportWebServiceId is set — not yet supported."); if (f.AnyDetailHasAttributeId) reasons.Add("A DrillDown detail row references AttributeId — 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) { // destinationSourceType is unused — DrillDown has no SourceType concept. var payload = System.Text.Json.JsonSerializer.Deserialize(bundle.PayloadJson) ?? throw new InvalidOperationException("DrillDown promotion payload could not be parsed."); int fromReportId = 0, toReportId = 0, moduleId = 0; if (!string.IsNullOrWhiteSpace(payload.FromReportCode)) fromReportId = await _ReportCodeLookup.GetReportIdByCode(payload.FromReportCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"No Report with ReportCode '{payload.FromReportCode}' exists at this destination."); if (!string.IsNullOrWhiteSpace(payload.ToReportCode)) toReportId = await _ReportCodeLookup.GetReportIdByCode(payload.ToReportCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"No Report with ReportCode '{payload.ToReportCode}' exists at this destination."); 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 dto = new DrillDownDTO { DrillDownId = existingDestinationId ?? 0, DrillDownCode = payload.DrillDownCode, DrillDownDescription = payload.DrillDownDescription, DrillDownType = payload.DrillDownType, DrillDownFromType = payload.DrillDownFromType, DrillDownToType = payload.DrillDownToType, FromReportId = fromReportId, ToReportId = toReportId, ModuleId = moduleId, DrillDownBIZTransactionTypeField = payload.DrillDownBIZTransactionTypeField, DrillDownSortOrder = payload.SortOrder, DrillDownDetailArray = payload.Details.Select(d => new DrillDownDetailDTO { DrillDownDetailSlNo = d.SlNo, DrillDownDetailMapType = d.MapType, AttributeId = -1, // never mapped from payload — see class header comment DrillDownDetailAssignType = d.AssignType, DrillDownDetailSourceField = d.SourceField, DrillDownDetailFilterValue = d.FilterValue, DrillDownDetailOperationType = d.OperationType, DrillDownDetailJoinType = d.JoinType }).ToList() }; await _DrillDownBLL.SaveDrillDown(dto, login).ConfigureAwait(false); return dto.DrillDownId; // mutated in place by SaveDrillDown when isNew (fixed bug, see DrillDownBLL.cs) } } }