using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.GOP; using GB5Shared.Deployment; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using Microsoft.Extensions.Logging; namespace FrameworkBLL.Promotion { // ============================================================ // PromotionFlowProvisioningBLL — one-time (idempotent) setup of the // "PromotionIngest" GOP flow: Ingest -> Approval -> Import. // // Authored via IGopFlowBLL's real design-time API (SaveGopFlow/ // SaveGopFlowStep/SaveGopFlowStepEdge/PublishGopFlow/SaveGopSourceBinding), // not hand-inserted MGOPFLOWSNAPSHOTSTEP rows via migration — PublishGopFlow // runs real DAG validation (GopPublishService: exactly one root step, no // cycles, every Approval step must carry an AssignedRole/AssignedToUserId) // that a raw-SQL seed would have to reimplement and could drift from. // // Idempotency: a no-op if an active source binding for SourceCode= // "PromotionIngest" already exists — safe to call repeatedly (deployment // startup, a one-time admin action, whatever the real rollout mechanism // ends up being). // ============================================================ public class PromotionFlowProvisioningBLL : IPromotionFlowProvisioningBLL { private const string FlowCode = "PromotionIngest"; private const string SourceCode = "PromotionIngest"; private const string SnapshotVersionLabel = "v1"; // bump deliberately if the flow shape changes // TODO: map to a real GB5 role/menu-rights concept at rollout time — this is a plain // string GopPublishService only checks for non-emptiness, not validated against any // role table today. private const string PromotionReviewerRole = "PROMOTIONREVIEWER"; private readonly IGopFlowBLL _GopFlowBLL; private readonly IGB5Environment _GB5Environment; private readonly ILogger _Logger; public PromotionFlowProvisioningBLL( IGopFlowBLL gopFlowBLL, IGB5Environment gb5Environment, ILogger logger) { _GopFlowBLL = gopFlowBLL; _GB5Environment = gb5Environment; _Logger = logger; } public async Task EnsureIngestFlowProvisionedAsync(LoginDTO login, CancellationToken ct) { // Same resolution GopQueueBLL.SubmitExecution/GopPublishService already use — // mirrored here (not shared) since it's each class's own small, server-side-only read. var environment = _GB5Environment.GopEnvironmentCode; var existingBindings = await _GopFlowBLL.GetAllSourceBindings(login).ConfigureAwait(false); var existingBinding = existingBindings.FirstOrDefault(b => b.SourceCode == SourceCode && b.IsActive); if (existingBinding is not null) { _Logger.LogInformation( "PromotionFlowProvisioningBLL: '{SourceCode}' already provisioned (FlowId={FlowId}, SnapshotVersion={Version}) — no-op.", SourceCode, existingBinding.FlowId, existingBinding.SnapshotVersion); return $"Already provisioned (FlowId={existingBinding.FlowId}, SnapshotVersion={existingBinding.SnapshotVersion})."; } var flows = await _GopFlowBLL.GetGopFlowList(login, ct).ConfigureAwait(false); var flow = flows.FirstOrDefault(f => f.FlowCode == FlowCode); int flowId; if (flow is null) { var newFlow = new GopFlowDTO { FlowCode = FlowCode, FlowName = "Metadata Promotion — Ingest, Approve, Import" }; await _GopFlowBLL.SaveGopFlow(newFlow, login, ct).ConfigureAwait(false); flowId = newFlow.FlowId; } else { flowId = flow.FlowId; } var graph = await _GopFlowBLL.GetGopFlowGraph(flowId, login, ct).ConfigureAwait(false); if (graph.Steps.Count == 0) { var ingestStep = new GopFlowStepDTO { FlowId = flowId, NodeCode = "ingest", NodeName = "Ingest & Validate", NodeType = "PromotionIngest" }; await _GopFlowBLL.SaveGopFlowStep(ingestStep, login, ct).ConfigureAwait(false); var approveStep = new GopFlowStepDTO { FlowId = flowId, NodeCode = "approve", NodeName = "Review & Approve", NodeType = "Approval", AssignedRole = PromotionReviewerRole }; await _GopFlowBLL.SaveGopFlowStep(approveStep, login, ct).ConfigureAwait(false); var importStep = new GopFlowStepDTO { FlowId = flowId, NodeCode = "import", NodeName = "Import", NodeType = "PromotionImport" }; await _GopFlowBLL.SaveGopFlowStep(importStep, login, ct).ConfigureAwait(false); await _GopFlowBLL.SaveGopFlowStepEdge(new GopFlowStepEdgeDTO { FlowId = flowId, FromStepId = ingestStep.FlowStepId, ToStepId = approveStep.FlowStepId, EdgeCondition = "OnSuccess" }, login, ct).ConfigureAwait(false); await _GopFlowBLL.SaveGopFlowStepEdge(new GopFlowStepEdgeDTO { FlowId = flowId, FromStepId = approveStep.FlowStepId, ToStepId = importStep.FlowStepId, EdgeCondition = "OnSuccess" }, login, ct).ConfigureAwait(false); _Logger.LogInformation( "PromotionFlowProvisioningBLL: created flow {FlowCode} (FlowId={FlowId}) with 3 steps.", FlowCode, flowId); } var publishResult = await _GopFlowBLL.PublishGopFlow(flowId, SnapshotVersionLabel, login, ct) .ConfigureAwait(false); if (!publishResult.IsSuccess) throw new InvalidOperationException( $"Failed to publish '{FlowCode}': {string.Join("; ", publishResult.Errors)}"); await _GopFlowBLL.SaveGopSourceBinding(new GopSourceBindingDTO { SourceCode = SourceCode, SourceType = "EVENT", SourceName = "Metadata Promotion Ingest", FlowId = flowId, SnapshotVersion = publishResult.SnapshotVersion, Environment = environment }, login, ct).ConfigureAwait(false); _Logger.LogInformation( "PromotionFlowProvisioningBLL: provisioned {SourceCode} -> FlowId={FlowId}, SnapshotVersion={Version}, Environment={Env}.", SourceCode, flowId, publishResult.SnapshotVersion, environment); return $"Provisioned (FlowId={flowId}, SnapshotVersion={publishResult.SnapshotVersion})."; } } }