Skip to content

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-flow clonado 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 ServicioInterfazEjemplo
Validacion de identidadIIdentityValidationProviderLiveness, OCR, facial match
Verificacion de datosIDataVerificationProviderListas de control, scoring
Firma digitalISignatureProviderFirmalo, Uanataca
NotificacionesINotificationProviderSMS, email, push
AlmacenamientoIStorageProviderS3, Azure Blob

Ejemplo con IIdentityValidationProvider:

csharp
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:

csharp
// 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

csharp
// 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

csharp
// 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:

csharp
// 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:

json
// 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:

csharp
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:

csharp
// 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

csharp
// 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:

bash
# 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

Reimagine Tech LLC — Documentacion Interna