Files
RealCV/src/TrueCV.Infrastructure/ExternalApis/CompaniesHouseClient.cs
peter 6d514e01b2 Initial commit: TrueCV CV verification platform
Clean architecture solution with:
- Domain: Entities (User, CVCheck, CVFlag, CompanyCache) and Enums
- Application: Service interfaces, DTOs, and models
- Infrastructure: EF Core, Identity, Hangfire, external API clients, services
- Web: Blazor Server UI with pages and components

Features:
- CV upload and parsing (PDF/DOCX) using Claude API
- Employment verification against Companies House API
- Timeline analysis for gaps and overlaps
- Veracity scoring algorithm
- Background job processing with Hangfire
- Azure Blob Storage for file storage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 19:20:50 +01:00

170 lines
6.0 KiB
C#

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TrueCV.Application.DTOs;
using TrueCV.Infrastructure.Configuration;
namespace TrueCV.Infrastructure.ExternalApis;
public sealed class CompaniesHouseClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<CompaniesHouseClient> _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
public CompaniesHouseClient(
HttpClient httpClient,
IOptions<CompaniesHouseSettings> settings,
ILogger<CompaniesHouseClient> logger)
{
_httpClient = httpClient;
_logger = logger;
var apiKey = settings.Value.ApiKey;
var authValue = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{apiKey}:"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authValue);
_httpClient.BaseAddress = new Uri(settings.Value.BaseUrl);
}
public async Task<CompaniesHouseSearchResponse?> SearchCompaniesAsync(
string query,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(query);
var encodedQuery = Uri.EscapeDataString(query);
var requestUrl = $"/search/companies?q={encodedQuery}";
_logger.LogDebug("Searching Companies House for: {Query}", query);
try
{
var response = await _httpClient.GetAsync(requestUrl, cancellationToken);
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.LogWarning("Rate limit exceeded for Companies House API");
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.");
}
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<CompaniesHouseSearchResponse>(
JsonOptions,
cancellationToken);
_logger.LogDebug("Found {Count} companies matching query: {Query}",
result?.Items?.Count ?? 0, query);
return result;
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.LogWarning("Rate limit exceeded for Companies House API");
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.", ex);
}
}
public async Task<CompaniesHouseCompany?> GetCompanyAsync(
string companyNumber,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(companyNumber);
var requestUrl = $"/company/{companyNumber}";
_logger.LogDebug("Fetching company details for: {CompanyNumber}", companyNumber);
try
{
var response = await _httpClient.GetAsync(requestUrl, cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
_logger.LogDebug("Company not found: {CompanyNumber}", companyNumber);
return null;
}
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.LogWarning("Rate limit exceeded for Companies House API");
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.");
}
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<CompaniesHouseCompany>(
JsonOptions,
cancellationToken);
_logger.LogDebug("Retrieved company: {CompanyName} ({CompanyNumber})",
result?.CompanyName, companyNumber);
return result;
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.LogWarning("Rate limit exceeded for Companies House API");
throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.", ex);
}
}
}
// DTOs for Companies House API responses
public sealed record CompaniesHouseSearchResponse
{
public int TotalResults { get; init; }
public int ItemsPerPage { get; init; }
public int StartIndex { get; init; }
public List<CompaniesHouseSearchItem> Items { get; init; } = [];
}
public sealed record CompaniesHouseSearchItem
{
public required string CompanyNumber { get; init; }
public required string Title { get; init; }
public string? CompanyStatus { get; init; }
public string? CompanyType { get; init; }
public string? DateOfCreation { get; init; }
public string? DateOfCessation { get; init; }
public CompaniesHouseAddress? Address { get; init; }
public string? AddressSnippet { get; init; }
}
public sealed record CompaniesHouseCompany
{
public required string CompanyNumber { get; init; }
public required string CompanyName { get; init; }
public string? CompanyStatus { get; init; }
public string? Type { get; init; }
public string? DateOfCreation { get; init; }
public string? DateOfCessation { get; init; }
public CompaniesHouseAddress? RegisteredOfficeAddress { get; init; }
}
public sealed record CompaniesHouseAddress
{
public string? Premises { get; init; }
public string? AddressLine1 { get; init; }
public string? AddressLine2 { get; init; }
public string? Locality { get; init; }
public string? Region { get; init; }
public string? PostalCode { get; init; }
public string? Country { get; init; }
}
public class CompaniesHouseRateLimitException : Exception
{
public CompaniesHouseRateLimitException(string message) : base(message) { }
public CompaniesHouseRateLimitException(string message, Exception innerException) : base(message, innerException) { }
}