using FrameworkBLL.EIPConversation.EIPHandlers.EIPConversationBLL; using FrameworkBLL.EIPConversation.EIPHandlers.EIPFlowRepository; using FrameworkDAL.CustomCode.DirectAction; using FrameworkDAL.CustomCode.EIPConversation.EIPSession; using FrameworkDAL.DTO.EIPConversation; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace FrameworkBLL.DirectAction { // ============================================================ // DirectActionConversationBLL — orchestrates the NeedsInput=1 conversational // handoff: a DirectActionDetail row with FlowId>0 hands off to a real EIP flow // instead of the static single-field remarks form. // // Identity: since the recipient clicked an anonymous email link (no login, no // chat session), the "user" for EIP session purposes is a synthetic identifier // derived from ActionCode+ContextId — stable across every GET/POST for the // same DirectAction instance, unique per instance. // // Context carry-through: on the very FIRST turn only, this pre-seeds the EIP // session's ContextJson with the DirectAction metadata (ActionCode, ContextId, // AssigneeUserId, TenantId, TokenHash, FrameworkBaseUrl) BEFORE the engine ever // runs. EIPConversationEngine restores ContextJson into context.Variables at // the start of every turn and re-saves the same dict at the end, so once // seeded, these keys survive every subsequent turn automatically — no need to // re-inject them after each call. This also means a flow that completes in a // single turn (no pause) still has the metadata available for its final // CAPABILITY step, which a "merge after the call returns" approach could not // guarantee. // ============================================================ public class DirectActionConversationBLL : IDirectActionConversationBLL { private readonly IDirectActionConfigDAL _configDAL; private readonly IEIPConversationBLL _conversationBLL; private readonly IEIPSessionDAL _sessionDAL; private readonly IEIPFlowRepository _flowRepository; private readonly ILogger _logger; public DirectActionConversationBLL( IDirectActionConfigDAL configDAL, IEIPConversationBLL conversationBLL, IEIPSessionDAL sessionDAL, IEIPFlowRepository flowRepository, ILogger logger) { _configDAL = configDAL ?? throw new ArgumentNullException(nameof(configDAL)); _conversationBLL = conversationBLL ?? throw new ArgumentNullException(nameof(conversationBLL)); _sessionDAL = sessionDAL ?? throw new ArgumentNullException(nameof(sessionDAL)); _flowRepository = flowRepository ?? throw new ArgumentNullException(nameof(flowRepository)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task ProcessAsync( string actionCode, int contextId, int assigneeUserId, int tenantId, string tokenHash, DateTime tokenExpiresAt, string frameworkBaseUrl, string? userMessage, LoginDTO loginDTO, CancellationToken ct = default) { var config = await _configDAL.GetDetailByActionCodeAsync(actionCode, loginDTO, ct) .ConfigureAwait(false); if (config is null || config.FlowId <= 0) throw new InvalidOperationException( $"No conversational flow configured for ActionCode '{actionCode}'."); var flowCode = await _configDAL.GetFlowCodeByFlowIdAsync(config.FlowId, tenantId, loginDTO, ct) .ConfigureAwait(false); if (string.IsNullOrWhiteSpace(flowCode)) throw new InvalidOperationException( $"FlowId {config.FlowId} on ActionCode '{actionCode}' has no Published flow definition."); var userIdentifier = $"DA:{actionCode}:{contextId}"; const int channelType = (int)EIPChannelType.DirectActionWeb; var existingSession = await _sessionDAL.GetActiveSessionAsync( userIdentifier, tenantId, channelType, loginDTO, ct).ConfigureAwait(false); if (existingSession is null) { var flowDefinition = await _flowRepository.GetFlowDefinitionAsync(flowCode, loginDTO, ct) .ConfigureAwait(false); if (flowDefinition is null || string.IsNullOrWhiteSpace(flowDefinition.StartStepCode)) throw new InvalidOperationException($"FlowCode '{flowCode}' could not be loaded."); var seedVariables = new Dictionary { ["_DirectAction.ActionCode"] = actionCode, ["_DirectAction.ContextId"] = contextId, ["_DirectAction.AssigneeUserId"] = assigneeUserId, ["_DirectAction.TenantId"] = tenantId, ["_DirectAction.TokenHash"] = tokenHash, ["_DirectAction.TokenExpiresAt"] = tokenExpiresAt.ToString("o"), ["_DirectAction.FrameworkBaseUrl"] = frameworkBaseUrl }; await _sessionDAL.UpsertSessionAsync(new EIPUserSessionDTO { UserIdentifier = userIdentifier, TenantId = tenantId, ChannelType = channelType, FlowCode = flowCode, CurrentStepCode = flowDefinition.StartStepCode, ContextJson = JsonSerializer.Serialize(seedVariables) }, loginDTO, ct).ConfigureAwait(false); _logger.LogInformation( "DirectAction conversation seeded | ActionCode={ActionCode} ContextId={ContextId} FlowCode={FlowCode}", actionCode, contextId, flowCode); } var payload = new EIPConversationDTO { TenantId = tenantId, ChannelType = EIPChannelType.DirectActionWeb, UserIdentifier = userIdentifier, Message = userMessage ?? string.Empty, FlowCode = flowCode, Channel = "DIRECTACTIONWEB", Platform = "DIRECTACTIONWEB" }; return await _conversationBLL.HandleIncomingConversationAsync(payload, loginDTO, ct) .ConfigureAwait(false); } } }