using GB5Shared.GOP.Qualifier; using GB5Shared.GOP.Qualifier.DTOs; using GB5Shared.GOP.Qualifier.Executors; using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.GOP; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using GB5Shared.DTO.Qualifier; using Microsoft.Extensions.Logging; namespace FrameworkBLL.GOP.Worker.NodeExecutors { // ============================================================ // QualifierNodeExecutor — executes a Qualifier node type. // // Deserializes the input payload as a JsonDocument, builds a // QualifierExecutionContext, and calls IQualifierFacadeImpl. // Returns the FactBag export serialized as JSON (becomes the // input payload for the next pipeline step). // // The FactBag is seeded with the raw input document's own // top-level scalar fields as Document facts BEFORE running any // qualifier methods, so a validation-only qualifier (no method // ever calls SetDocumentFact) doesn't silently discard the // original row for the next pipeline step — a downstream Mapper // reading "[FactBag] SomeField" still finds it. Any method that // does call SetDocumentFact for the same key overrides this // baseline, since it runs after this seeding. // // If qualifier returns errors → throws (pipeline will retry // or route to dead-letter per MaxRetries). // ============================================================ public class QualifierNodeExecutor : INodeExecutor { private readonly IQualifierFacadeImpl _QualifierFacade; private readonly ILogger _Logger; public QualifierNodeExecutor( IQualifierFacadeImpl qualifierFacade, ILogger logger) { _QualifierFacade = qualifierFacade; _Logger = logger; } public string NodeType => "Qualifier"; public async Task ExecuteAsync( GopFlowSnapshotStepDTO step, GopExecutionHeaderDTO header, string? inputPayloadJson, LoginDTO loginDTO, CancellationToken ct) { using var document = string.IsNullOrWhiteSpace(inputPayloadJson) ? JsonDocument.Parse("{}") : JsonDocument.Parse(inputPayloadJson); var facts = new QualifierFactBag(); if (document.RootElement.ValueKind == JsonValueKind.Object) { foreach (var prop in document.RootElement.EnumerateObject()) { var scalar = ExtractScalar(prop.Value); if (scalar is not null) facts.SetDocumentFact(prop.Name, scalar); } } var ctx = new QualifierExecutionContext { ClientId = loginDTO.ClientId, EntityId = header.ExecutionId, EntityCode = "GOPEXECUTION", Stage = 1, // PreValidate — callers can pass stage via step config in future Scope = 1, Document = document, Facts = facts, CorrelationId = header.ExecutionId.ToString(), CancellationToken = ct }; var result = await _QualifierFacade.ExecuteStageAsync(ctx, loginDTO, ct); if (result.HasErrors) { var errorSummary = string.Join("; ", result.Findings .Where(f => f.Severity == FindingSeverity.Error) .Select(f => $"[{f.Code}] {f.Message}")); _Logger.LogWarning( "QualifierNodeExecutor: execution {ExecutionId} has {ErrorCount} errors: {Errors}", header.ExecutionId, result.Findings.Count, errorSummary); throw new InvalidOperationException( $"Qualifier validation failed with errors: {errorSummary}"); } _Logger.LogDebug( "QualifierNodeExecutor: execution {ExecutionId} passed with {FindingCount} findings", header.ExecutionId, result.Findings.Count); // Return the hierarchical FactBag as the next step's input. return JsonSerializer.Serialize(result.Facts); } private static object? ExtractScalar(JsonElement element) => element.ValueKind switch { JsonValueKind.String => element.GetString(), JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, _ => null }; } }