using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FrameworkDAL.CustomCode.Promotion; using FrameworkDAL.DTO.Promotion; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using GB5Shared.DTO.Promotion; using GB5Shared.EventLogPublish; using GB5Shared.Promotion; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace FrameworkBLL.GOP.Worker.NodeExecutors { // ============================================================ // PromotionImportNodeExecutor — executes a "PromotionImport" node type. // // Runs only after GOP's Approval gate has let the flow through. Calls the // destination's own IPromotableEntity.ImportPromotedAsync — which in turn // calls that entity's own existing Save method (MenuBLL.Save, // PortletTypeBLL.Save, etc.), so Promotion never writes an entity row // directly; it reuses each entity's real validation, AutoNumber // assignment, and (once wired) audit trail. // // DestinationSourceType (2=DevAdmin or 3=ImpAdmin) is server-side config on // THIS deployment, never accepted from the incoming payload — a // deployment's own tier in the DEVSYS/IMPSYS/Client hierarchy is fixed for // that install, same rule GopQueueBLL.SubmitExecution already follows for // GOP:Environment. // ============================================================ public class PromotionImportNodeExecutor : INodeExecutor { // Seed rows to add to MEVENTTYPE alongside this: EVENTCATEGORY=Promotion, // ISBIZDOMAIN=1, ISAUDIT=1 — see the plan's Observability section. private const string AssetPromotedEventTypeConfigKey = "Promotion:AssetPromotedEventTypeId"; private readonly IServiceProvider _ServiceProvider; private readonly IPromotionAssetLinkDAL _AssetLinkDal; private readonly IConfiguration _Configuration; private readonly EventLogPublish _EventLogPublish; private readonly ILogger _Logger; public PromotionImportNodeExecutor( IServiceProvider serviceProvider, IPromotionAssetLinkDAL assetLinkDal, IConfiguration configuration, EventLogPublish eventLogPublish, ILogger logger) { _ServiceProvider = serviceProvider; _AssetLinkDal = assetLinkDal; _Configuration = configuration; _EventLogPublish = eventLogPublish; _Logger = logger; } public string NodeType => "PromotionImport"; public async Task ExecuteAsync( GopFlowSnapshotStepDTO step, GopExecutionHeaderDTO header, string? inputPayloadJson, LoginDTO loginDTO, CancellationToken ct) { if (string.IsNullOrWhiteSpace(inputPayloadJson)) throw new InvalidOperationException("PromotionImportNodeExecutor requires an ingest-result payload."); var ingestResult = JsonSerializer.Deserialize(inputPayloadJson) ?? throw new InvalidOperationException("Promotion ingest result could not be parsed."); var destinationSourceTypeSetting = _Configuration["Promotion:DestinationSourceType"]; if (!byte.TryParse(destinationSourceTypeSetting, out var destinationSourceType)) throw new InvalidOperationException( "Promotion:DestinationSourceType is not configured on this deployment " + "(2=DevAdmin, 3=ImpAdmin) — required before Import can run."); var bundle = ingestResult.Bundle; var entity = _ServiceProvider.GetKeyedService(bundle.EntityTypeCode) ?? throw new InvalidOperationException( $"No IPromotableEntity registered for EntityTypeCode '{bundle.EntityTypeCode}'."); var destinationEntityId = await entity.ImportPromotedAsync( bundle, destinationSourceType, ingestResult.ExistingDestinationId, loginDTO, ct) .ConfigureAwait(false); var linkDto = new PromotionAssetLinkDTO { OriginEnvironmentCode = bundle.OriginEnvironmentCode, EntityTypeCode = bundle.EntityTypeCode, OriginEntityId = bundle.OriginEntityId, DestinationEntityId = destinationEntityId, LastGopExecutionId = header.ExecutionId, LastPromotedOn = DateTime.UtcNow }; if (ingestResult.ExistingDestinationId.HasValue) { var existing = await _AssetLinkDal.GetByOrigin( bundle.OriginEnvironmentCode, bundle.EntityTypeCode, bundle.OriginEntityId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( "MPROMOTIONASSETLINK row disappeared between Ingest and Import."); linkDto.PromotionAssetLinkId = existing.PromotionAssetLinkId; await _AssetLinkDal.UpdateAfterPromotion(linkDto, loginDTO, ct).ConfigureAwait(false); } else { await _AssetLinkDal.Insert(linkDto, loginDTO, ct).ConfigureAwait(false); } var eventTypeIdSetting = _Configuration[AssetPromotedEventTypeConfigKey]; if (int.TryParse(eventTypeIdSetting, out var eventTypeId)) { await _EventLogPublish.PublishEventLogAsync( "Metadata Promotion — Asset Promoted", new { bundle.EntityTypeCode, bundle.OriginEntityId, bundle.OriginEnvironmentCode, DestinationEntityId = destinationEntityId, IsUpdate = ingestResult.IsUpdateToExistingAsset }, eventTypeId, destinationEntityId, loginDTO).ConfigureAwait(false); } else { _Logger.LogWarning( "PromotionImportNodeExecutor: execution {ExecutionId} — {ConfigKey} is not configured, " + "skipping business EventLogPublish (GOP's own Approve/Execute events still fire).", header.ExecutionId, AssetPromotedEventTypeConfigKey); } _Logger.LogInformation( "PromotionImportNodeExecutor: execution {ExecutionId} promoted {EntityTypeCode}/{OriginEntityId} " + "-> DestinationEntityId={DestinationEntityId} (isUpdate={IsUpdate})", header.ExecutionId, bundle.EntityTypeCode, bundle.OriginEntityId, destinationEntityId, ingestResult.IsUpdateToExistingAsset); return JsonSerializer.Serialize(new { destinationEntityId }); } } }