using System.Net.Http.Json; using System.Text.Json; using BizProcessBLL.Interfaces; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using static GB5Shared.DTO.WorkFlow.ContractsDTO; namespace BizProcessBLL.Implementations { /// /// GB5 implementation of — reaches the Framework /// host's real, existing WorkFlow engine over HTTP (named client "framework", registered in /// Program.cs), not a direct C# project reference. BizProcessSL and FrameworkSL are separate /// deployed hosts (see feedback_cross_host_di_boundary) — the same IHttpClientFactory /// named-client pattern already used by DXP→MM (MmIntegrationService.cs) and /// Framework→Entitlement (EntitlementGateClient.cs) is followed here, not Dapr service /// invocation (confirmed via research: every actively-maintained cross-host call in this /// repo uses this pattern; the one DaprClient.InvokeMethodAsync call found elsewhere is /// leftover example code, not a followed convention). /// /// EntityId resolution: GetWorkflowConfigAsync fetches the referenced MBIZTRANSACTIONCLASS /// record (also over HTTP, on the same "framework" client — GetBIZTransactionClass is /// Framework-hosted too) purely to read its ENTITYID column and forward it into /// WorkflowCheckContext. This turned out to matter in practice, not just in principle: every /// real MWORKFLOWCONFIG row on GB5DEMO is entity-scoped (ENTITYID != -1), so skipping this /// resolution and passing -1 would make the lookup silently never match anything real. /// /// GetWorkflow's response body is a JSON string with real property names still carrying the /// "Workflow" prefix from FrameworkDAL's WorkflowDetailDTO (WorkflowApprovalLevel, /// WorkflowDisplayName, etc., inside a WorkflowDetailArray) — that DTO is Framework-private, /// so this deserializes via JsonDocument rather than taking a compile-time dependency on it. /// public class Gb5ProcessOperationalDataSource : IProcessOperationalDataSource { private readonly IHttpClientFactory _HttpClientFactory; private readonly ILogger _Logger; public Gb5ProcessOperationalDataSource(IHttpClientFactory httpClientFactory, ILogger logger) { _HttpClientFactory = httpClientFactory; _Logger = logger; } public async Task GetWorkflowConfigAsync( int bizTransactionClassId, int bizTransactionId, LoginDTO login, CancellationToken ct) { try { var client = _HttpClientFactory.CreateClient("framework"); client.DefaultRequestHeaders.Remove("Login"); client.DefaultRequestHeaders.Add("Login", JsonSerializer.Serialize(login)); var entityId = await ResolveEntityIdAsync(client, bizTransactionClassId, ct).ConfigureAwait(false); var context = new WorkflowCheckContext { EntityId = entityId, OUId = -1, BizTransactionClassId = bizTransactionClassId, BizTransactionId = bizTransactionId, }; var response = await client.PostAsJsonAsync("WorkFlow/CheckApplicability", context, ct).ConfigureAwait(false); if (!response.IsSuccessStatusCode) return null; var envelope = await response.Content.ReadFromJsonAsync(cancellationToken: ct).ConfigureAwait(false); var bodyRaw = envelope?.RootElement.GetProperty("Body").GetString(); if (string.IsNullOrWhiteSpace(bodyRaw)) return null; // CheckWorkflowApplicability double-encodes its body (JsonConvert.SerializeObject // into the Body string) — same quirk already confirmed for GetBIZTransactionClass. var result = JsonSerializer.Deserialize(bodyRaw, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (result is null || !result.IsEnabled) return null; return new WorkflowLookupResult { IsEnabled = true, WorkFlowId = result.WorkFlowId }; } catch (Exception ex) { // A workflow-lookup failure must never block rendering the process map itself. GB5Trace.MarkFailed("get-workflow-config-failed", ex); _Logger.LogWarning(ex, "GetWorkflowConfigAsync failed for BizTransactionClassId {ClassId} BizTransactionId {TransId}", bizTransactionClassId, bizTransactionId); return null; } } /// /// -1 (wildcard) when bizTransactionClassId itself is -1 (unlinked stage), or on any /// failure to reach/parse the class record — a resolution failure here should degrade to /// "no entity scoping" rather than blocking the workflow lookup entirely. /// private async Task ResolveEntityIdAsync(HttpClient client, int bizTransactionClassId, CancellationToken ct) { if (bizTransactionClassId == -1) return -1; try { var response = await client.GetAsync($"BIZTransactionClass/GetBIZTransactionClass?BizTransactionClassId={bizTransactionClassId}", ct).ConfigureAwait(false); if (!response.IsSuccessStatusCode) return -1; var envelope = await response.Content.ReadFromJsonAsync(cancellationToken: ct).ConfigureAwait(false); if (!envelope!.RootElement.TryGetProperty("Body", out var bodyEl)) return -1; // GetBIZTransactionClass double-encodes its Body as a JSON string — same quirk // already confirmed live from the FE side of this same lookup. var classEl = bodyEl.ValueKind == JsonValueKind.String ? JsonDocument.Parse(bodyEl.GetString()!).RootElement : bodyEl; return classEl.TryGetProperty("EntityId", out var eid) ? eid.GetInt32() : -1; } catch (Exception ex) { GB5Trace.MarkFailed("resolve-entityid-failed", ex); _Logger.LogWarning(ex, "ResolveEntityIdAsync failed for BizTransactionClassId {ClassId}", bizTransactionClassId); return -1; } } public async Task> GetWorkflowStepsAsync(int workflowId, LoginDTO login, CancellationToken ct) { var steps = new List(); try { var client = _HttpClientFactory.CreateClient("framework"); client.DefaultRequestHeaders.Remove("Login"); client.DefaultRequestHeaders.Add("Login", JsonSerializer.Serialize(login)); var response = await client.GetAsync($"Workflow/GetWorkflow?WorkflowId={workflowId}", ct).ConfigureAwait(false); if (!response.IsSuccessStatusCode) return steps; var envelope = await response.Content.ReadFromJsonAsync(cancellationToken: ct).ConfigureAwait(false); if (!envelope!.RootElement.TryGetProperty("Body", out var bodyEl)) return steps; // GetWorkflow's Body is the real (non-double-encoded) object here, but its exact // element kind depends on the string-vs-object quirk already seen elsewhere in // this codebase — handle both defensively rather than assuming. var workflowEl = bodyEl.ValueKind == JsonValueKind.String ? JsonDocument.Parse(bodyEl.GetString()!).RootElement : bodyEl; if (!workflowEl.TryGetProperty("WorkflowDetailArray", out var detailArray) || detailArray.ValueKind != JsonValueKind.Array) return steps; foreach (var d in detailArray.EnumerateArray()) { steps.Add(new WorkflowStepSummaryDTO { ApprovalLevel = d.TryGetProperty("WorkflowApprovalLevel", out var lvl) ? lvl.GetInt32() : 0, DisplayName = d.TryGetProperty("WorkflowDisplayName", out var dn) ? dn.GetString() : null, StepType = d.TryGetProperty("WorkflowStepType", out var st) ? st.GetInt32() : 0, ParallelGroupKey = d.TryGetProperty("WorkflowParallelGroupKey", out var pgk) ? pgk.GetString() : null, EvalCondition = d.TryGetProperty("WorkflowEvalCondition", out var ec) ? ec.GetString() : null, }); } } catch (Exception ex) { GB5Trace.MarkFailed("get-workflow-steps-failed", ex); _Logger.LogWarning(ex, "GetWorkflowStepsAsync failed for WorkflowId {WorkflowId}", workflowId); } return steps; } /// /// Reaches MM's existing GetDocumentTraceability endpoint (a separate host, named client /// "MMModule" — same cross-host HTTP pattern as "framework", registered in Program.cs and /// already proven by DXP's MmIntegrationService.cs). The endpoint's own pivot-parsing /// (ParsePivotAllocationId) reads AllocationId off a CriteriaDTO's first section/attribute, /// so that's the exact shape built here rather than a simpler query param. /// public async Task> GetDocumentTraceAsync(int pivotAllocationId, LoginDTO login, CancellationToken ct) { var rows = new List(); try { var client = _HttpClientFactory.CreateClient("MMModule"); client.DefaultRequestHeaders.Remove("Login"); client.DefaultRequestHeaders.Add("Login", JsonSerializer.Serialize(login)); var criteria = new CriteriaDTO { SectionCriteriaList = new List { new SectionCriteriaDTO { AttributesCriteriaList = new List { new AttributesCriteriaDTO { FieldName = "AllocationId", FieldValue = pivotAllocationId } } } } }; var response = await client.PostAsJsonAsync( "Register/DocumentTraceability?FirstNumber=0&MaxResult=1000", criteria, ct).ConfigureAwait(false); if (!response.IsSuccessStatusCode) return rows; var envelope = await response.Content.ReadFromJsonAsync(cancellationToken: ct).ConfigureAwait(false); if (!envelope!.RootElement.TryGetProperty("Body", out var bodyEl)) return rows; var traceEl = bodyEl.ValueKind == JsonValueKind.String ? JsonDocument.Parse(bodyEl.GetString()!).RootElement : bodyEl; if (traceEl.ValueKind != JsonValueKind.Array) return rows; // Property names below match DocumentTraceabilityDTO's literal C# names exactly // (GB5's global JSON naming policy is null -> PascalCase, no camelCasing) -- // including its "ParentAllocationid" and "BIZTransactionTypeId" quirky casing. foreach (var r in traceEl.EnumerateArray()) { rows.Add(new DocumentTraceRowDTO { AllocationId = r.TryGetProperty("AllocationId", out var aid) ? aid.GetInt32() : 0, ParentAllocationId = r.TryGetProperty("ParentAllocationid", out var pid) ? pid.GetInt32() : -1, DocumentLevel = r.TryGetProperty("DocumentLevel", out var lvl) ? lvl.GetInt32() : 0, BizTransactionTypeId = r.TryGetProperty("BIZTransactionTypeId", out var btt) ? btt.GetInt32() : -1, DocumentNumber = r.TryGetProperty("DocumentNumber", out var dn) ? dn.GetString() : null, DocumentDate = r.TryGetProperty("DocumentDate", out var dd) && dd.ValueKind != JsonValueKind.Null ? dd.GetDateTime() : default, ObjectHeaderTypeId = r.TryGetProperty("ObjectHeaderTypeId", out var oht) ? oht.GetInt32() : -1, ObjectHeaderId = r.TryGetProperty("ObjectHeaderId", out var ohi) ? ohi.GetInt32() : -1, }); } } catch (Exception ex) { GB5Trace.MarkFailed("get-document-trace-failed", ex); _Logger.LogWarning(ex, "GetDocumentTraceAsync failed for PivotAllocationId {PivotAllocationId}", pivotAllocationId); } return rows; } /// /// Reaches MM's new GetPendingCountsByTransactionType endpoint (same "MMModule" named /// client Phase 7 already registered) -- deliberately NOT GetMMPendingStatusReport, which /// groups by BizTransactionClassId and can't answer "how many open documents for /// transaction type X" (see that new MM endpoint's own doc comment for the full reason). /// public async Task> GetOpenCountsAsync(IEnumerable bizTransactionTypeIds, LoginDTO login, CancellationToken ct) { var counts = new List(); var ids = bizTransactionTypeIds.Distinct().ToList(); if (ids.Count == 0) return counts; try { var client = _HttpClientFactory.CreateClient("MMModule"); client.DefaultRequestHeaders.Remove("Login"); client.DefaultRequestHeaders.Add("Login", JsonSerializer.Serialize(login)); var idsParam = string.Join(",", ids); var response = await client.GetAsync( $"MMReports/GetPendingCountsByTransactionType?BizTransactionTypeIds={idsParam}", ct).ConfigureAwait(false); if (!response.IsSuccessStatusCode) return counts; var envelope = await response.Content.ReadFromJsonAsync(cancellationToken: ct).ConfigureAwait(false); if (!envelope!.RootElement.TryGetProperty("Body", out var bodyEl)) return counts; var arrEl = bodyEl.ValueKind == JsonValueKind.String ? JsonDocument.Parse(bodyEl.GetString()!).RootElement : bodyEl; if (arrEl.ValueKind != JsonValueKind.Array) return counts; foreach (var r in arrEl.EnumerateArray()) { counts.Add(new StageOpenCountDTO { BizTransactionTypeId = r.TryGetProperty("BizTransactionTypeId", out var btt) ? btt.GetInt32() : -1, OpenCount = r.TryGetProperty("OpenCount", out var oc) ? oc.GetInt32() : 0, }); } } catch (Exception ex) { GB5Trace.MarkFailed("get-open-counts-failed", ex); _Logger.LogWarning(ex, "GetOpenCountsAsync failed for {Count} BizTransactionTypeIds", ids.Count); } return counts; } } }