Guia: Agregar un Nuevo Proveedor Externo a ImagFlow
Esta guia describe el proceso completo para integrar un nuevo proveedor externo al Provider Gateway de ImagFlow, desde la implementacion del adapter hasta la verificacion con health check.
Prerequisitos
- Repositorio
imagy-flowclonado y compilando - Documentacion de la API del proveedor externo disponible
- Credenciales de sandbox/testing del proveedor
- Conocimiento del Provider Gateway de ImagFlow
Contexto: Provider Gateway
ImagFlow abstrae proveedores externos mediante el patron Adapter. Cada proveedor implementa una interfaz comun segun el tipo de servicio que ofrece:
1. Identificar la Interfaz del Proveedor
Segun el tipo de servicio, el proveedor debe implementar una de estas interfaces:
| Tipo de Servicio | Interfaz | Ejemplo |
|---|---|---|
| Validacion de identidad | IIdentityValidationProvider | Liveness, OCR, facial match |
| Verificacion de datos | IDataVerificationProvider | Listas de control, scoring |
| Firma digital | ISignatureProvider | Firmalo, Uanataca |
| Notificaciones | INotificationProvider | SMS, email, push |
| Almacenamiento | IStorageProvider | S3, Azure Blob |
Ejemplo con IIdentityValidationProvider:
public interface IIdentityValidationProvider
{
string ProviderCode { get; }
string ServiceType { get; }
string[] SupportedCountries { get; }
Task<ValidationInitResult> InitiateValidationAsync(
ValidationRequest request, CancellationToken ct);
Task<ValidationStatus> GetStatusAsync(
string externalId, CancellationToken ct);
Task<ValidationResult> GetResultAsync(
string externalId, CancellationToken ct);
Task<HealthCheckResult> CheckHealthAsync(CancellationToken ct);
}2. Implementar el Adapter
Crear el adapter en el proyecto de infraestructura:
// src/Imagy.Flow.Infrastructure/Providers/{ProviderName}/{ProviderName}Adapter.cs
namespace Imagy.Flow.Infrastructure.Providers.NuevoProveedor;
public class NuevoProveedorAdapter : IIdentityValidationProvider
{
private readonly HttpClient _httpClient;
private readonly NuevoProveedorConfig _config;
private readonly ILogger<NuevoProveedorAdapter> _logger;
public string ProviderCode => "nuevo-proveedor-co";
public string ServiceType => "identity_validation";
public string[] SupportedCountries => ["CO"];
public NuevoProveedorAdapter(
HttpClient httpClient,
IOptions<NuevoProveedorConfig> config,
ILogger<NuevoProveedorAdapter> logger)
{
_httpClient = httpClient;
_config = config.Value;
_logger = logger;
}
public async Task<ValidationInitResult> InitiateValidationAsync(
ValidationRequest request, CancellationToken ct)
{
_logger.LogInformation(
"Initiating validation with {Provider} for {Country}",
ProviderCode, request.Country);
var externalRequest = MapToExternalRequest(request);
var response = await _httpClient.PostAsJsonAsync(
"/api/v1/validations", externalRequest, ct);
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadFromJsonAsync<ExternalValidationResponse>(ct);
return new ValidationInitResult
{
ExternalId = result!.TransactionId,
Status = ValidationStatus.InProgress,
RedirectUrl = result.ValidationUrl,
ExpiresAt = DateTime.UtcNow.AddMinutes(_config.SessionTimeoutMinutes)
};
}
public async Task<ValidationStatus> GetStatusAsync(
string externalId, CancellationToken ct)
{
var response = await _httpClient.GetAsync(
$"/api/v1/validations/{externalId}/status", ct);
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadFromJsonAsync<ExternalStatusResponse>(ct);
return MapToInternalStatus(result!.Status);
}
public async Task<ValidationResult> GetResultAsync(
string externalId, CancellationToken ct)
{
var response = await _httpClient.GetAsync(
$"/api/v1/validations/{externalId}/result", ct);
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadFromJsonAsync<ExternalResultResponse>(ct);
return MapToInternalResult(result!);
}
public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken ct)
{
try
{
var response = await _httpClient.GetAsync("/health", ct);
return response.IsSuccessStatusCode
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy($"Status: {response.StatusCode}");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(ex.Message);
}
}
// --- Mappers privados ---
private static ExternalValidationRequest MapToExternalRequest(ValidationRequest request)
{
return new ExternalValidationRequest
{
DocumentType = request.DocumentType,
DocumentNumber = request.DocumentNumber,
FullName = request.FullName,
CallbackUrl = request.CallbackUrl
};
}
private static ValidationStatus MapToInternalStatus(string externalStatus)
{
return externalStatus switch
{
"pending" => ValidationStatus.InProgress,
"completed" => ValidationStatus.Completed,
"failed" => ValidationStatus.Failed,
"expired" => ValidationStatus.Expired,
_ => ValidationStatus.Unknown
};
}
private static ValidationResult MapToInternalResult(ExternalResultResponse external)
{
return new ValidationResult
{
IsValid = external.Approved,
Score = external.ConfidenceScore,
Details = new ValidationDetails
{
LivenessScore = external.LivenessScore,
FacialMatchScore = external.FaceMatchScore,
DocumentValid = external.DocumentVerified
},
RawResponse = JsonSerializer.Serialize(external)
};
}
}3. Crear la Configuracion
// src/Imagy.Flow.Infrastructure/Providers/{ProviderName}/{ProviderName}Config.cs
namespace Imagy.Flow.Infrastructure.Providers.NuevoProveedor;
public class NuevoProveedorConfig
{
public string BaseUrl { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public string ApiSecret { get; set; } = string.Empty;
public int SessionTimeoutMinutes { get; set; } = 15;
public int HttpTimeoutSeconds { get; set; } = 30;
public string WebhookSecret { get; set; } = string.Empty;
}4. Registrar en Dependency Injection
// src/Imagy.Flow.Infrastructure/DependencyInjection/ProviderRegistration.cs
public static class ProviderRegistration
{
public static IServiceCollection AddNuevoProveedor(
this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<NuevoProveedorConfig>(
configuration.GetSection("Providers:NuevoProveedor"));
services.AddHttpClient<NuevoProveedorAdapter>(client =>
{
var config = configuration
.GetSection("Providers:NuevoProveedor")
.Get<NuevoProveedorConfig>()!;
client.BaseAddress = new Uri(config.BaseUrl);
client.DefaultRequestHeaders.Add("X-Api-Key", config.ApiKey);
client.Timeout = TimeSpan.FromSeconds(config.HttpTimeoutSeconds);
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
// Registrar en el registry de proveedores
services.AddSingleton<IIdentityValidationProvider, NuevoProveedorAdapter>();
return services;
}
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}
}5. Crear Registro en Base de Datos
Crear una migracion para insertar la configuracion del proveedor:
// Migracion: AddNuevoProveedorConfig
public partial class AddNuevoProveedorConfig : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
INSERT INTO provider_configs (
id, provider_code, provider_name, service_type,
country, connection_config, is_active, created_at
) VALUES (
gen_random_uuid(),
'nuevo-proveedor-co',
'Nuevo Proveedor Colombia',
'identity_validation',
'CO',
'{""base_url"": ""https://api.nuevoproveedor.co/v1"", ""timeout_seconds"": 30}'::jsonb,
true,
NOW()
);
");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
DELETE FROM provider_configs WHERE provider_code = 'nuevo-proveedor-co';
");
}
}6. Configurar Failover
Si el proveedor tiene un backup o alternativa, configurar failover:
// appsettings.json
{
"Providers": {
"NuevoProveedor": {
"BaseUrl": "https://api.nuevoproveedor.co/v1",
"ApiKey": "{{from-secrets-manager}}",
"ApiSecret": "{{from-secrets-manager}}",
"SessionTimeoutMinutes": 15,
"HttpTimeoutSeconds": 30
},
"Failover": {
"nuevo-proveedor-co": {
"fallback_provider": "proveedor-alternativo-co",
"trigger_after_failures": 3,
"cooldown_seconds": 300
}
}
}
}Implementar el failover en el Provider Gateway:
public class ProviderGatewayService
{
private readonly IProviderRegistry _registry;
private readonly IFailoverPolicy _failoverPolicy;
public async Task<ValidationInitResult> InitiateAsync(
string providerCode, ValidationRequest request, CancellationToken ct)
{
try
{
var provider = _registry.GetProvider<IIdentityValidationProvider>(providerCode);
return await provider.InitiateValidationAsync(request, ct);
}
catch (ProviderUnavailableException)
{
var fallback = _failoverPolicy.GetFallback(providerCode);
if (fallback is null) throw;
var fallbackProvider = _registry.GetProvider<IIdentityValidationProvider>(fallback);
return await fallbackProvider.InitiateValidationAsync(request, ct);
}
}
}7. Implementar Webhook (si aplica)
Si el proveedor notifica resultados via webhook:
// src/Imagy.Flow.Api/Endpoints/WebhookEndpoints.cs
public static class WebhookEndpoints
{
public static void MapProviderWebhooks(this IEndpointRouteBuilder app)
{
app.MapPost("/api/v1/webhooks/providers/{providerCode}/callback",
HandleProviderCallback)
.AllowAnonymous();
}
private static async Task<IResult> HandleProviderCallback(
string providerCode,
HttpContext context,
IWebhookProcessor processor,
CancellationToken ct)
{
// 1. Verificar firma HMAC
var signature = context.Request.Headers["X-Webhook-Signature"].FirstOrDefault();
var body = await new StreamReader(context.Request.Body).ReadToEndAsync(ct);
if (!await processor.VerifySignatureAsync(providerCode, body, signature))
return Results.Unauthorized();
// 2. Procesar callback
await processor.ProcessAsync(providerCode, body, ct);
return Results.Ok(new { received = true });
}
}8. Test con Health Check
// tests/Imagy.Flow.Tests/Integration/NuevoProveedorHealthCheckTest.cs
public class NuevoProveedorHealthCheckTest
{
[Fact]
public async Task HealthCheck_WhenProviderIsUp_ReturnsHealthy()
{
// Arrange
using var mockServer = WireMockServer.Start();
mockServer.Given(Request.Create().WithPath("/health").UsingGet())
.RespondWith(Response.Create().WithStatusCode(200));
var adapter = CreateAdapter(mockServer.Url);
// Act
var result = await adapter.CheckHealthAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(HealthStatus.Healthy);
}
[Fact]
public async Task HealthCheck_WhenProviderIsDown_ReturnsUnhealthy()
{
// Arrange
using var mockServer = WireMockServer.Start();
mockServer.Given(Request.Create().WithPath("/health").UsingGet())
.RespondWith(Response.Create().WithStatusCode(503));
var adapter = CreateAdapter(mockServer.Url);
// Act
var result = await adapter.CheckHealthAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
}
[Fact]
public async Task InitiateValidation_WithValidRequest_ReturnsExternalId()
{
// Arrange
using var mockServer = WireMockServer.Start();
mockServer.Given(Request.Create().WithPath("/api/v1/validations").UsingPost())
.RespondWith(Response.Create()
.WithStatusCode(201)
.WithBody("""
{
"transaction_id": "ext-123",
"validation_url": "https://validate.provider.co/ext-123",
"expires_at": "2026-01-01T00:00:00Z"
}
"""));
var adapter = CreateAdapter(mockServer.Url);
var request = new ValidationRequest
{
DocumentType = "cedula",
DocumentNumber = "1234567890",
FullName = "Juan Perez",
Country = "CO"
};
// Act
var result = await adapter.InitiateValidationAsync(request, CancellationToken.None);
// Assert
result.ExternalId.Should().Be("ext-123");
result.Status.Should().Be(ValidationStatus.InProgress);
result.RedirectUrl.Should().NotBeNullOrEmpty();
}
private static NuevoProveedorAdapter CreateAdapter(string baseUrl)
{
var httpClient = new HttpClient { BaseAddress = new Uri(baseUrl) };
var config = Options.Create(new NuevoProveedorConfig
{
BaseUrl = baseUrl,
ApiKey = "test-key",
SessionTimeoutMinutes = 15,
HttpTimeoutSeconds = 30
});
var logger = NullLogger<NuevoProveedorAdapter>.Instance;
return new NuevoProveedorAdapter(httpClient, config, logger);
}
}9. Verificar via API (Platform Admin)
Una vez desplegado, el platform admin puede verificar el proveedor:
# Health check del proveedor
curl -X POST https://api.{tenant}.reimaginetech.io/v1/providers/nuevo-proveedor-co/health-check \
-H "Authorization: Bearer {platform_admin_jwt}"
# Respuesta esperada
{
"provider_code": "nuevo-proveedor-co",
"status": "healthy",
"response_time_ms": 245,
"checked_at": "2026-05-18T10:00:00Z"
}Checklist
- [ ] Interfaz correcta identificada segun tipo de servicio
- [ ] Adapter implementado con todos los metodos de la interfaz
- [ ] Configuracion externalizada (no hardcoded)
- [ ] Registrado en DI con HttpClient tipado
- [ ] Retry policy y circuit breaker configurados
- [ ] Registro en base de datos (provider_configs)
- [ ] Failover configurado (si aplica)
- [ ] Webhook implementado con verificacion HMAC (si aplica)
- [ ] Health check funcional
- [ ] Tests de integracion con WireMock
- [ ] Secrets almacenados en AWS Secrets Manager (no en appsettings)
- [ ] Documentacion del proveedor actualizada
Referencias
- ImagFlow Provider Gateway — Arquitectura del gateway
- ImagFlow Overview — Vision general del dominio
- Guia: Nuevo Servicio — Si necesitas crear un servicio nuevo
- Seguridad — Manejo de secrets y credenciales