feat: implement building address table from csv
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user