feat: implement building address table from csv

This commit is contained in:
Stevan Freeborn
2024-08-24 16:06:44 -05:00
parent 86527efad9
commit 0b9a83619d
4 changed files with 154 additions and 12 deletions
@@ -2,6 +2,10 @@ namespace SanctionsSearch.Worker.Tests.Integration;
public class DatabaseMaintainerTests : DatabaseTest
{
private const string TestAddressCsv = """
306,201,"Dai-Ichi Bldg. 6th Floor, 10-2 Nihombashi, 2-chome, Chuo-ku","Tokyo 103","Japan",-0-
306,202,"Federico Boyd Avenue & 51 Street","Panama City","Panama",-0-
""";
private readonly MockHttpMessageHandler _mockHttp = new();
private readonly OfacFileServiceOptionsFaker _ofacFileServiceOptionsFaker = new();
private readonly OfacFileServiceOptions _ofacFileServiceOptions;
@@ -83,4 +87,71 @@ public class DatabaseMaintainerTests : DatabaseTest
UpdatedAt = now.DateTime
});
}
[Fact]
public async Task BuildAddressTableAsync_WhenCalledAndNoSdnRecordFound_ItShouldNotAddAddressCsvRecordsToDatabase()
{
var testStream = CreateCsvStream(TestAddressCsv);
_mockHttp
.When(_ofacFileServiceOptions.GetAddressFileUri().ToString())
.Respond("text/csv", testStream);
await _databaseMaintainer.BuildAddressTableAsync();
var addresses = await _context.Set<Address>().ToListAsync();
addresses.Should().BeEmpty();
}
[Fact]
public async Task BuildAddressTableAsync_WhenCalledAndSdnRecordFound_ItShouldAddAddressCsvRecordsToDatabase()
{
var testStream = CreateCsvStream(TestAddressCsv);
_mockHttp
.When(_ofacFileServiceOptions.GetAddressFileUri().ToString())
.Respond("text/csv", testStream);
var now = DateTimeOffset.UtcNow;
_timeProviderMock
.Setup(x => x.GetUtcNow())
.Returns(now);
var sdn = new Sdn() { Id = 306 };
await _context.Set<Sdn>().AddAsync(sdn);
await _context.SaveChangesAsync();
await _databaseMaintainer.BuildAddressTableAsync();
var addresses = await _context.Set<Address>().ToListAsync();
addresses.Should().BeEquivalentTo(new[]
{
new Address()
{
SdnId = sdn.Id,
Id = 201,
StreetAddress = "Dai-Ichi Bldg. 6th Floor, 10-2 Nihombashi, 2-chome, Chuo-ku",
CityProvincePostal = "Tokyo 103",
Country = "Japan",
CreatedAt = now.DateTime,
UpdatedAt = now.DateTime,
Sdn = sdn
},
new Address()
{
SdnId = sdn.Id,
Id = 202,
StreetAddress = "Federico Boyd Avenue & 51 Street",
CityProvincePostal = "Panama City",
Country = "Panama",
CreatedAt = now.DateTime,
UpdatedAt = now.DateTime,
Sdn = sdn
}
});
}
}
@@ -0,0 +1,14 @@
namespace SanctionsSearch.Worker.Models;
class AddressMap : ClassMap<Address>
{
public AddressMap()
{
Map(m => m.SdnId).Index(0);
Map(m => m.Id).Index(1);
Map(m => m.StreetAddress).Index(2).TypeConverter<NullCharacterConverter>();
Map(m => m.CityProvincePostal).Index(3).TypeConverter<NullCharacterConverter>();
Map(m => m.Country).Index(4).TypeConverter<NullCharacterConverter>();
Map(m => m.Remarks).Index(5).TypeConverter<NullCharacterConverter>();
}
}
@@ -4,11 +4,28 @@ class DatabaseMaintainer(
IUnitOfWork unitOfWork,
IOfacFileService ofacFileService,
ILogger<DatabaseMaintainer> logger
) : IDatabaseMaintainer
) : IDatabaseMaintainer, IDisposable
{
private readonly IUnitOfWork _unitOfWork = unitOfWork;
private readonly IOfacFileService _ofacFileService = ofacFileService;
private readonly ILogger<DatabaseMaintainer> _logger = logger;
private readonly CsvConfiguration _csvConfig = new(CultureInfo.InvariantCulture) { HasHeaderRecord = false };
private readonly List<CsvReader> _csvReaders = [];
private readonly List<StreamReader> _streamReaders = [];
private IEnumerable<T> GetRecordsFromStream<T>(Stream stream)
{
var reader = new StreamReader(stream);
var csv = new CsvReader(reader, _csvConfig);
csv.Context.RegisterClassMap<SdnMap>();
csv.Context.RegisterClassMap<AddressMap>();
_streamReaders.Add(reader);
_csvReaders.Add(csv);
return csv.GetRecords<T>();
}
public async Task BuildSdnTableAsync()
{
@@ -19,14 +36,9 @@ class DatabaseMaintainer(
_logger.LogError("Failed to get SDN file from OFAC.");
return;
}
using var stream = result.Value;
// TODO: Abstract this logic to wrapper around CsvHelper
using var reader = new StreamReader(stream);
var config = new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = false };
using var csv = new CsvReader(reader, config);
csv.Context.RegisterClassMap<SdnMap>();
var records = csv.GetRecords<Sdn>();
using var stream = result.Value;
var records = GetRecordsFromStream<Sdn>(stream);
foreach (var record in records)
{
@@ -36,9 +48,33 @@ class DatabaseMaintainer(
await _unitOfWork.SaveChangesAsync();
}
public Task BuildAddressTableAsync()
public async Task BuildAddressTableAsync()
{
throw new NotImplementedException();
var result = await _ofacFileService.GetAddressFileAsync();
if (result.IsFailed)
{
_logger.LogError("Failed to get Address file from OFAC.");
return;
}
using var stream = result.Value;
var records = GetRecordsFromStream<Address>(stream);
foreach (var record in records)
{
var sdn = await _unitOfWork.Sdns.Find(s => s.Id == record.SdnId);
if (sdn.Count() is 0)
{
_logger.LogWarning("Address's SDN with ID {Id} not found. Skipping address.", record.SdnId);
continue;
}
await _unitOfWork.Addresses.Upsert(record);
}
await _unitOfWork.SaveChangesAsync();
}
public Task BuildCommentTableAsync()
@@ -50,4 +86,10 @@ class DatabaseMaintainer(
{
throw new NotImplementedException();
}
public void Dispose()
{
_csvReaders.ForEach(csv => csv.Dispose());
_streamReaders.ForEach(reader => reader.Dispose());
}
}
@@ -3,18 +3,33 @@ namespace SanctionsSearch.Worker.Persistence;
class EfUnitOfWork(DbContext context, ILoggerFactory loggerFactory) : IUnitOfWork, IAsyncDisposable
{
private readonly DbContext _context = context;
private readonly ILogger<EfUnitOfWork> _logger = loggerFactory.CreateLogger<EfUnitOfWork>();
public ISdnRepository Sdns { get; } = new SdnRepository(context, loggerFactory.CreateLogger<SdnRepository>());
public IAddressRepository Addresses { get; } = new AddressRepository(context, loggerFactory.CreateLogger<AddressRepository>());
public IAliasRepository Aliases { get; } = new AliasRepository(context, loggerFactory.CreateLogger<AliasRepository>());
public ICommentRepository Comments { get; } = new CommentRepository(context, loggerFactory.CreateLogger<CommentRepository>());
public async Task SaveChangesAsync()
{
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "Failed to save changes to the database.");
}
}
public async ValueTask DisposeAsync()
{
try
{
await _context.DisposeAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to dispose of the database context.");
}
}
}