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:
342
src/RealCV.Infrastructure/Clients/OrcidClient.cs
Normal file
342
src/RealCV.Infrastructure/Clients/OrcidClient.cs
Normal file
@@ -0,0 +1,342 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace RealCV.Infrastructure.Clients;
|
||||
|
||||
public sealed class OrcidClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<OrcidClient> _logger;
|
||||
|
||||
public OrcidClient(
|
||||
HttpClient httpClient,
|
||||
ILogger<OrcidClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
_httpClient.BaseAddress = new Uri("https://pub.orcid.org/v3.0/");
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
|
||||
public async Task<OrcidSearchResponse?> SearchResearchersAsync(string query, int start = 0, int rows = 20)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encodedQuery = Uri.EscapeDataString(query);
|
||||
var url = $"search?q={encodedQuery}&start={start}&rows={rows}";
|
||||
|
||||
_logger.LogDebug("Searching ORCID: {Query}", query);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("ORCID API returned {StatusCode} for search: {Query}",
|
||||
response.StatusCode, query);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<OrcidSearchResponse>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching ORCID: {Query}", query);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OrcidRecord?> GetRecordAsync(string orcidId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Normalize ORCID ID format (remove URL prefix if present)
|
||||
orcidId = NormalizeOrcidId(orcidId);
|
||||
|
||||
var url = $"{orcidId}/record";
|
||||
|
||||
_logger.LogDebug("Getting ORCID record: {OrcidId}", orcidId);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("ORCID API returned {StatusCode} for ID: {OrcidId}",
|
||||
response.StatusCode, orcidId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<OrcidRecord>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting ORCID record: {OrcidId}", orcidId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OrcidWorks?> GetWorksAsync(string orcidId)
|
||||
{
|
||||
try
|
||||
{
|
||||
orcidId = NormalizeOrcidId(orcidId);
|
||||
var url = $"{orcidId}/works";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<OrcidWorks>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting ORCID works: {OrcidId}", orcidId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OrcidEmployments?> GetEmploymentsAsync(string orcidId)
|
||||
{
|
||||
try
|
||||
{
|
||||
orcidId = NormalizeOrcidId(orcidId);
|
||||
var url = $"{orcidId}/employments";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<OrcidEmployments>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting ORCID employments: {OrcidId}", orcidId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OrcidEducations?> GetEducationsAsync(string orcidId)
|
||||
{
|
||||
try
|
||||
{
|
||||
orcidId = NormalizeOrcidId(orcidId);
|
||||
var url = $"{orcidId}/educations";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<OrcidEducations>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting ORCID educations: {OrcidId}", orcidId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeOrcidId(string orcidId)
|
||||
{
|
||||
// Remove URL prefixes
|
||||
orcidId = orcidId.Replace("https://orcid.org/", "")
|
||||
.Replace("http://orcid.org/", "")
|
||||
.Trim();
|
||||
return orcidId;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
}
|
||||
|
||||
// Response models
|
||||
public class OrcidSearchResponse
|
||||
{
|
||||
[JsonPropertyName("num-found")]
|
||||
public int NumFound { get; set; }
|
||||
|
||||
public List<OrcidSearchResult>? Result { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidSearchResult
|
||||
{
|
||||
[JsonPropertyName("orcid-identifier")]
|
||||
public OrcidIdentifier? OrcidIdentifier { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidIdentifier
|
||||
{
|
||||
public string? Uri { get; set; }
|
||||
public string? Path { get; set; }
|
||||
public string? Host { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidRecord
|
||||
{
|
||||
[JsonPropertyName("orcid-identifier")]
|
||||
public OrcidIdentifier? OrcidIdentifier { get; set; }
|
||||
|
||||
public OrcidPerson? Person { get; set; }
|
||||
|
||||
[JsonPropertyName("activities-summary")]
|
||||
public OrcidActivitiesSummary? ActivitiesSummary { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidPerson
|
||||
{
|
||||
public OrcidName? Name { get; set; }
|
||||
public OrcidBiography? Biography { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidName
|
||||
{
|
||||
[JsonPropertyName("given-names")]
|
||||
public OrcidValue? GivenNames { get; set; }
|
||||
|
||||
[JsonPropertyName("family-name")]
|
||||
public OrcidValue? FamilyName { get; set; }
|
||||
|
||||
[JsonPropertyName("credit-name")]
|
||||
public OrcidValue? CreditName { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidValue
|
||||
{
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidBiography
|
||||
{
|
||||
public string? Content { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidActivitiesSummary
|
||||
{
|
||||
public OrcidEmployments? Employments { get; set; }
|
||||
public OrcidEducations? Educations { get; set; }
|
||||
public OrcidWorks? Works { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidEmployments
|
||||
{
|
||||
[JsonPropertyName("affiliation-group")]
|
||||
public List<OrcidAffiliationGroup>? AffiliationGroup { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidEducations
|
||||
{
|
||||
[JsonPropertyName("affiliation-group")]
|
||||
public List<OrcidAffiliationGroup>? AffiliationGroup { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidAffiliationGroup
|
||||
{
|
||||
public List<OrcidAffiliationSummaryWrapper>? Summaries { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidAffiliationSummaryWrapper
|
||||
{
|
||||
[JsonPropertyName("employment-summary")]
|
||||
public OrcidAffiliationSummary? EmploymentSummary { get; set; }
|
||||
|
||||
[JsonPropertyName("education-summary")]
|
||||
public OrcidAffiliationSummary? EducationSummary { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidAffiliationSummary
|
||||
{
|
||||
[JsonPropertyName("department-name")]
|
||||
public string? DepartmentName { get; set; }
|
||||
|
||||
[JsonPropertyName("role-title")]
|
||||
public string? RoleTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("start-date")]
|
||||
public OrcidDate? StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("end-date")]
|
||||
public OrcidDate? EndDate { get; set; }
|
||||
|
||||
public OrcidOrganization? Organization { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidDate
|
||||
{
|
||||
public OrcidValue? Year { get; set; }
|
||||
public OrcidValue? Month { get; set; }
|
||||
public OrcidValue? Day { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidOrganization
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public OrcidAddress? Address { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidAddress
|
||||
{
|
||||
public string? City { get; set; }
|
||||
public string? Region { get; set; }
|
||||
public string? Country { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidWorks
|
||||
{
|
||||
public List<OrcidWorkGroup>? Group { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidWorkGroup
|
||||
{
|
||||
[JsonPropertyName("work-summary")]
|
||||
public List<OrcidWorkSummary>? WorkSummary { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidWorkSummary
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public OrcidTitle? TitleObj { get; set; }
|
||||
public string? Type { get; set; }
|
||||
|
||||
[JsonPropertyName("publication-date")]
|
||||
public OrcidDate? PublicationDate { get; set; }
|
||||
|
||||
[JsonPropertyName("journal-title")]
|
||||
public OrcidValue? JournalTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("external-ids")]
|
||||
public OrcidExternalIds? ExternalIds { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidTitle
|
||||
{
|
||||
public OrcidValue? Title { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidExternalIds
|
||||
{
|
||||
[JsonPropertyName("external-id")]
|
||||
public List<OrcidExternalId>? ExternalId { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidExternalId
|
||||
{
|
||||
[JsonPropertyName("external-id-type")]
|
||||
public string? ExternalIdType { get; set; }
|
||||
|
||||
[JsonPropertyName("external-id-value")]
|
||||
public string? ExternalIdValue { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user