using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FrameworkDAL.CustomCode.Promotion; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using GB5Shared.DTO.Promotion; using GB5Shared.Promotion; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace FrameworkBLL.GOP.Worker.NodeExecutors { // ============================================================ // PromotionIngestNodeExecutor — executes a "PromotionIngest" node type. // // Destination-side step of the Metadata Promotion flow. Input payload: the // PromotionBundleDTO produced by the origin's PromotionExport step, however // it physically arrived here (Tier 1 manual upload, Tier 2 Exchange, or // Tier 3 direct API — this executor doesn't care which). // // Re-runs validation independently — never trusts only the origin's copy — // and checks MPROMOTIONASSETLINK for a prior promotion of the same // (OriginEnvironmentCode, EntityTypeCode, OriginEntityId) triple, so a // re-promotion of an updated version is recognized as an update rather than // a duplicate. Output feeds GOP's built-in Approval gate next (a separate // flow step, NodeType="Approval" — GopExecutionPipeline pauses there before // ever dispatching to an INodeExecutor). // ============================================================ public class PromotionIngestNodeExecutor : INodeExecutor { private readonly IServiceProvider _ServiceProvider; private readonly IPromotionAssetLinkDAL _AssetLinkDal; private readonly IConfiguration _Configuration; private readonly ILogger _Logger; public PromotionIngestNodeExecutor( IServiceProvider serviceProvider, IPromotionAssetLinkDAL assetLinkDal, IConfiguration configuration, ILogger logger) { _ServiceProvider = serviceProvider; _AssetLinkDal = assetLinkDal; _Configuration = configuration; _Logger = logger; } public string NodeType => "PromotionIngest"; public async Task ExecuteAsync( GopFlowSnapshotStepDTO step, GopExecutionHeaderDTO header, string? inputPayloadJson, LoginDTO loginDTO, CancellationToken ct) { if (string.IsNullOrWhiteSpace(inputPayloadJson)) throw new InvalidOperationException("PromotionIngestNodeExecutor requires a bundle payload."); var bundle = JsonSerializer.Deserialize(inputPayloadJson) ?? throw new InvalidOperationException("Promotion bundle could not be parsed."); // Own environment identity is server-side config, never accepted from the // incoming payload — same rule GopQueueBLL.SubmitExecution already follows for // GOP:Environment. var thisEnvironmentCode = _Configuration["Promotion:OriginEnvironmentCode"]; if (string.IsNullOrWhiteSpace(thisEnvironmentCode)) _Logger.LogWarning( "PromotionIngestNodeExecutor: execution {ExecutionId} — Promotion:OriginEnvironmentCode " + "is not configured on this deployment.", header.ExecutionId); var entity = _ServiceProvider.GetKeyedService(bundle.EntityTypeCode) ?? throw new InvalidOperationException( $"No IPromotableEntity registered for EntityTypeCode '{bundle.EntityTypeCode}'."); var validation = await entity.ValidateForPromotionAsync(bundle, ct).ConfigureAwait(false); if (!validation.IsPromotable) { var reasons = string.Join("; ", validation.BlockingReasons); _Logger.LogWarning( "PromotionIngestNodeExecutor: execution {ExecutionId} blocked at ingest — {Reasons}", header.ExecutionId, reasons); throw new InvalidOperationException($"Not promotable: {reasons}"); } var existingLink = await _AssetLinkDal.GetByOrigin( bundle.OriginEnvironmentCode, bundle.EntityTypeCode, bundle.OriginEntityId, loginDTO, ct).ConfigureAwait(false); var result = new PromotionIngestResultDTO { Bundle = bundle, ExistingDestinationId = existingLink?.DestinationEntityId, Validation = validation }; _Logger.LogInformation( "PromotionIngestNodeExecutor: execution {ExecutionId} ingested {EntityTypeCode}/{OriginEntityId} " + "from {OriginEnvironmentCode} — isUpdate={IsUpdate}", header.ExecutionId, bundle.EntityTypeCode, bundle.OriginEntityId, bundle.OriginEnvironmentCode, result.IsUpdateToExistingAsset); return JsonSerializer.Serialize(result); } } }