feat: Add additional verification APIs (FCA, SRA, GitHub, OpenCorporates, ORCID)
This adds five new free API integrations for enhanced CV verification: - FCA Register API: Verify financial services professionals - SRA Register API: Verify solicitors and legal professionals - GitHub API: Verify developer profiles and technical skills - OpenCorporates API: Verify international companies across jurisdictions - ORCID API: Verify academic researchers and publications Includes: - API clients for all five services with retry policies - Service implementations with name matching and validation - Models for verification results with detailed flags - Configuration options in appsettings.json - DI registration for all services 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
216
src/RealCV.Infrastructure/Clients/FcaRegisterClient.cs
Normal file
216
src/RealCV.Infrastructure/Clients/FcaRegisterClient.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace RealCV.Infrastructure.Clients;
|
||||
|
||||
public sealed class FcaRegisterClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<FcaRegisterClient> _logger;
|
||||
private readonly string _apiKey;
|
||||
|
||||
public FcaRegisterClient(
|
||||
HttpClient httpClient,
|
||||
IOptions<FcaOptions> options,
|
||||
ILogger<FcaRegisterClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_apiKey = options.Value.ApiKey;
|
||||
|
||||
_httpClient.BaseAddress = new Uri("https://register.fca.org.uk/services/V0.1/");
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Auth-Email", options.Value.Email);
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Auth-Key", _apiKey);
|
||||
}
|
||||
|
||||
public async Task<FcaIndividualResponse?> SearchIndividualsAsync(string name, int page = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encodedName = Uri.EscapeDataString(name);
|
||||
var url = $"Individuals?q={encodedName}&page={page}";
|
||||
|
||||
_logger.LogDebug("Searching FCA for individual: {Name}", name);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("FCA API returned {StatusCode} for search: {Name}",
|
||||
response.StatusCode, name);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<FcaIndividualResponse>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching FCA for individual: {Name}", name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FcaIndividualDetails?> GetIndividualAsync(string individualReferenceNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"Individuals/{individualReferenceNumber}";
|
||||
|
||||
_logger.LogDebug("Getting FCA individual: {IRN}", individualReferenceNumber);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("FCA API returned {StatusCode} for IRN: {IRN}",
|
||||
response.StatusCode, individualReferenceNumber);
|
||||
return null;
|
||||
}
|
||||
|
||||
var wrapper = await response.Content.ReadFromJsonAsync<FcaIndividualDetailsWrapper>(JsonOptions);
|
||||
return wrapper?.Data?.FirstOrDefault();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting FCA individual: {IRN}", individualReferenceNumber);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FcaFirmResponse?> SearchFirmsAsync(string name, int page = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encodedName = Uri.EscapeDataString(name);
|
||||
var url = $"Firms?q={encodedName}&page={page}";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<FcaFirmResponse>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching FCA for firm: {Name}", name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
}
|
||||
|
||||
public class FcaOptions
|
||||
{
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// Response models
|
||||
public class FcaIndividualResponse
|
||||
{
|
||||
public List<FcaIndividualSearchItem>? Data { get; set; }
|
||||
public FcaPagination? Pagination { get; set; }
|
||||
}
|
||||
|
||||
public class FcaIndividualSearchItem
|
||||
{
|
||||
[JsonPropertyName("Individual Reference Number")]
|
||||
public string? IndividualReferenceNumber { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
public string? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("Current Employer(s)")]
|
||||
public string? CurrentEmployers { get; set; }
|
||||
}
|
||||
|
||||
public class FcaIndividualDetailsWrapper
|
||||
{
|
||||
public List<FcaIndividualDetails>? Data { get; set; }
|
||||
}
|
||||
|
||||
public class FcaIndividualDetails
|
||||
{
|
||||
[JsonPropertyName("Individual Reference Number")]
|
||||
public string? IndividualReferenceNumber { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
public string? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("Effective Date")]
|
||||
public string? EffectiveDate { get; set; }
|
||||
|
||||
[JsonPropertyName("Controlled Functions")]
|
||||
public List<FcaControlledFunction>? ControlledFunctions { get; set; }
|
||||
|
||||
[JsonPropertyName("Previous Employments")]
|
||||
public List<FcaPreviousEmployment>? PreviousEmployments { get; set; }
|
||||
}
|
||||
|
||||
public class FcaControlledFunction
|
||||
{
|
||||
[JsonPropertyName("Controlled Function")]
|
||||
public string? ControlledFunction { get; set; }
|
||||
|
||||
[JsonPropertyName("Firm Name")]
|
||||
public string? FirmName { get; set; }
|
||||
|
||||
[JsonPropertyName("Firm Reference Number")]
|
||||
public string? FirmReferenceNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("Status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("Effective From")]
|
||||
public string? EffectiveFrom { get; set; }
|
||||
}
|
||||
|
||||
public class FcaPreviousEmployment
|
||||
{
|
||||
[JsonPropertyName("Firm Name")]
|
||||
public string? FirmName { get; set; }
|
||||
|
||||
[JsonPropertyName("Firm Reference Number")]
|
||||
public string? FirmReferenceNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("Start Date")]
|
||||
public string? StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("End Date")]
|
||||
public string? EndDate { get; set; }
|
||||
}
|
||||
|
||||
public class FcaFirmResponse
|
||||
{
|
||||
public List<FcaFirmSearchItem>? Data { get; set; }
|
||||
public FcaPagination? Pagination { get; set; }
|
||||
}
|
||||
|
||||
public class FcaFirmSearchItem
|
||||
{
|
||||
[JsonPropertyName("Firm Reference Number")]
|
||||
public string? FirmReferenceNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("Firm Name")]
|
||||
public string? FirmName { get; set; }
|
||||
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
|
||||
public class FcaPagination
|
||||
{
|
||||
public int Page { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
public int TotalItems { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user