using GB5Shared.ActionProcessor.Query; using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace GB5Shared.ActionProcessor { public interface IWebhookEndpointResolver { /// /// Host (and, unlike config, optionally scheme) to substitute for a template's /// literal {BaseURI} token, taking precedence over /// SysJobSettings:Gb4ServiceBaseUrl when supplied. Pass the live incoming /// request's HttpContext.Request.Host ("zero config" — see BaseEndpoint's /// RequestUrl capture / ActionExecuteEndpoint's frameworkBaseUrl for the same /// pattern) when the target is reachable through the same gateway the caller /// itself came in on. Leave null to keep the existing static-config behavior. /// /// /// MSERVERCONFIG.CONNECTIONNAME for this tenant — NOT the physical database name. /// ApplicationConnection resolves DB connections by this value, not DatabaseName; /// passing the raw database name here fails with "No server configuration found". /// Task ResolveAsync( int webServiceId, string? uriParameterValue, int tenantId, string connectionName, CancellationToken ct = default, string? baseUriOverride = null); // MWEBSERVICE.REQUESTSCHEMATYPE — 0 means "batch" (caller submits every row as one list in // a single call), 1 (the default, including every pre-existing row where the column is // NULL) means "per-row", i.e. today's existing one-call-per-row behavior. Only IceImport's // MenuWebServiceDispatcher consults this today; WebhookActionHandler (SysJob) has no batch // concept and never calls it. Task GetRequestSchemaTypeAsync(int webServiceId, int tenantId, string databaseName, CancellationToken ct = default); } /// /// Resolves a full webhook URL by loading MWEBSERVICE.URITEMPLATE/SECONDURITEMPLATE /// for the given WebServiceId, substituting a literal {BaseURI} token (if present) /// with either the caller-supplied baseUriOverride or the configured base URL, then /// substituting any remaining {param} placeholders with UriParameterValue. /// /// Config key: SysJobSettings:Gb4ServiceBaseUrl (host[:port], no scheme — the /// templates already carry a literal "http://" ahead of {BaseURI}). /// public sealed class WebhookEndpointResolver : IWebhookEndpointResolver { private static readonly Regex _placeholder = new( @"\{[^}]+\}", RegexOptions.Compiled); private readonly IQueryExecutor _qe; private readonly IConfiguration _config; private readonly ILogger _logger; public WebhookEndpointResolver( IQueryExecutor qe, IConfiguration config, ILogger logger) { _qe = qe; _config = config; _logger = logger; } public async Task ResolveAsync( int webServiceId, string? uriParameterValue, int tenantId, string connectionName, CancellationToken ct = default, string? baseUriOverride = null) { try { // ✅ DatabaseName here must hold the CONNECTION NAME (matches how a real, // live user session's LoginDTO.DatabaseName is populated by AuthenticationBLL, // and what ApplicationConnection.DBConnectionStringCached keys its lookup on) // — not the tenant's literal database name. var login = new LoginDTO { ClientId = tenantId, DatabaseName = connectionName }; var row = await _qe.QuerySingleAsync( login, WebhookEndpointQB.GET_WEBSERVICE_URL, new { WebServiceId = webServiceId, TenantId = tenantId }, cancellationToken: ct).ConfigureAwait(false); if (row is null || string.IsNullOrWhiteSpace(row.UriTemplate)) { _logger.LogWarning( "WebhookEndpointResolver: no active MWEBSERVICE for WebServiceId={Id} TenantId={T}", webServiceId, tenantId); return null; } // SECONDURITEMPLATE, when set, takes priority over URITEMPLATE, but is NOT // necessarily already a complete URL — it can carry the exact same literal // {BaseURI} token URITEMPLATE does (e.g. "http://{BaseURI}/prs/TimeSlip/ // GetTimeSlip/?TimeSlipId={TimeSlipId}"). Route it through the SAME // {BaseURI}-then-{param} resolution below instead of a bare, single-value // regex replace — the old code here replaced {BaseURI} itself with // uriParameterValue (e.g. an ObjectId), producing a URL with the record's ID // as its HOST. Both templates share one resolution path from this point on. var template = !string.IsNullOrWhiteSpace(row.SecondUriTemplate) ? row.SecondUriTemplate : row.UriTemplate; // {BaseURI} must be resolved BEFORE the generic {param} substitution below: // _placeholder's regex matches any {...} (including {BaseURI} itself), so // resolving it first keeps the two placeholder kinds from clobbering each other. if (template.Contains("{BaseURI}")) { // baseUriOverride (the caller's own live request Host — "zero config", // same convention as BaseEndpoint.RequestUrl / ActionExecuteEndpoint's // frameworkBaseUrl) wins when supplied; falls back to static config so // existing non-HTTP-triggered callers (SysJob) keep working unchanged. var baseUrl = !string.IsNullOrWhiteSpace(baseUriOverride) ? baseUriOverride.TrimEnd('/') : _config["SysJobSettings:Gb4ServiceBaseUrl"]?.TrimEnd('/') ?? string.Empty; if (string.IsNullOrEmpty(baseUrl)) { _logger.LogWarning( "WebhookEndpointResolver: no baseUriOverride supplied and " + "SysJobSettings:Gb4ServiceBaseUrl not configured; cannot resolve template={T}", template); return null; } template = template.Replace("{BaseURI}", baseUrl); } // Substitute any remaining {placeholder} with UriParameterValue if provided if (!string.IsNullOrEmpty(uriParameterValue)) template = _placeholder.Replace(template, uriParameterValue); return template; } catch (OperationCanceledException) { throw; } catch (Exception ex) { _logger.LogError( ex, "WebhookEndpointResolver: failed to resolve URL for WebServiceId={Id} TenantId={T}", webServiceId, tenantId); return null; } } public async Task GetRequestSchemaTypeAsync( int webServiceId, int tenantId, string databaseName, CancellationToken ct = default) { try { var login = new LoginDTO { ClientId = tenantId, DatabaseName = databaseName }; return await _qe.QuerySingleAsync( login, WebhookEndpointQB.GET_REQUEST_SCHEMA_TYPE, new { WebServiceId = webServiceId }, cancellationToken: ct).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (Exception ex) { _logger.LogError( ex, "WebhookEndpointResolver: failed to resolve RequestSchemaType for WebServiceId={Id} TenantId={T} — defaulting to per-row (1)", webServiceId, tenantId); return 1; } } private sealed class WebServiceUrlRow { public string? UriTemplate { get; set; } public string? SecondUriTemplate { get; set; } } } }