feat: complete end to end functionality
This commit is contained in:
@@ -1,63 +0,0 @@
|
|||||||
namespace SanctionsSearch.Worker.Tests.Unit;
|
|
||||||
|
|
||||||
public class AddressRepositoryTests
|
|
||||||
{
|
|
||||||
private readonly Mock<DbContext> _contextMock = new();
|
|
||||||
private readonly Mock<ILogger<AddressRepository>> _loggerMock = new();
|
|
||||||
private readonly AddressFaker _faker = new();
|
|
||||||
private readonly AddressRepository _repository;
|
|
||||||
|
|
||||||
public AddressRepositoryTests()
|
|
||||||
{
|
|
||||||
_repository = new AddressRepository(_contextMock.Object, _loggerMock.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Upsert_WhenExceptionIsThrown_ItShouldLogError()
|
|
||||||
{
|
|
||||||
var entity = _faker.Generate();
|
|
||||||
var mockSet = new Mock<DbSet<Address>>();
|
|
||||||
|
|
||||||
mockSet
|
|
||||||
.Setup(x => x.FindAsync(entity.Id))
|
|
||||||
.Throws<Exception>();
|
|
||||||
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.Set<Address>())
|
|
||||||
.Returns(mockSet.Object);
|
|
||||||
|
|
||||||
await _repository.Upsert(entity);
|
|
||||||
|
|
||||||
_loggerMock.Verify(
|
|
||||||
x => x.Log(
|
|
||||||
LogLevel.Error,
|
|
||||||
It.IsAny<EventId>(),
|
|
||||||
It.IsAny<It.IsAnyType>(),
|
|
||||||
It.IsAny<Exception>(),
|
|
||||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Find_WhenExceptionIsThrown_ItShouldReturnEmptyListAndLogError()
|
|
||||||
{
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.Set<Address>())
|
|
||||||
.Throws<Exception>();
|
|
||||||
|
|
||||||
var result = await _repository.Find(x => x.Id == 1);
|
|
||||||
|
|
||||||
result.Should().BeEmpty();
|
|
||||||
|
|
||||||
_loggerMock.Verify(
|
|
||||||
x => x.Log(
|
|
||||||
LogLevel.Error,
|
|
||||||
It.IsAny<EventId>(),
|
|
||||||
It.IsAny<It.IsAnyType>(),
|
|
||||||
It.IsAny<Exception>(),
|
|
||||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
namespace SanctionsSearch.Worker.Tests.Unit;
|
|
||||||
|
|
||||||
public class AliasRepositoryTests
|
|
||||||
{
|
|
||||||
private readonly Mock<DbContext> _contextMock = new();
|
|
||||||
private readonly Mock<ILogger<AliasRepository>> _loggerMock = new();
|
|
||||||
private readonly AliasFaker _faker = new();
|
|
||||||
private readonly AliasRepository _repository;
|
|
||||||
|
|
||||||
public AliasRepositoryTests()
|
|
||||||
{
|
|
||||||
_repository = new AliasRepository(_contextMock.Object, _loggerMock.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Upsert_WhenExceptionIsThrown_ItShouldLogError()
|
|
||||||
{
|
|
||||||
var entity = _faker.Generate();
|
|
||||||
var mockSet = new Mock<DbSet<Alias>>();
|
|
||||||
|
|
||||||
mockSet
|
|
||||||
.Setup(x => x.FindAsync(entity.Id))
|
|
||||||
.Throws<Exception>();
|
|
||||||
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.Set<Alias>())
|
|
||||||
.Returns(mockSet.Object);
|
|
||||||
|
|
||||||
await _repository.Upsert(entity);
|
|
||||||
|
|
||||||
_loggerMock.Verify(
|
|
||||||
x => x.Log(
|
|
||||||
LogLevel.Error,
|
|
||||||
It.IsAny<EventId>(),
|
|
||||||
It.IsAny<It.IsAnyType>(),
|
|
||||||
It.IsAny<Exception>(),
|
|
||||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Find_WhenExceptionIsThrown_ItShouldReturnEmptyListAndLogError()
|
|
||||||
{
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.Set<Alias>())
|
|
||||||
.Throws<Exception>();
|
|
||||||
|
|
||||||
var result = await _repository.Find(x => x.Id == 1);
|
|
||||||
|
|
||||||
result.Should().BeEmpty();
|
|
||||||
|
|
||||||
_loggerMock.Verify(
|
|
||||||
x => x.Log(
|
|
||||||
LogLevel.Error,
|
|
||||||
It.IsAny<EventId>(),
|
|
||||||
It.IsAny<It.IsAnyType>(),
|
|
||||||
It.IsAny<Exception>(),
|
|
||||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
namespace SanctionsSearch.Worker.Tests.Unit;
|
|
||||||
|
|
||||||
public class CommentRepositoryTests
|
|
||||||
{
|
|
||||||
private readonly Mock<DbContext> _contextMock = new();
|
|
||||||
private readonly Mock<ILogger<CommentRepository>> _loggerMock = new();
|
|
||||||
private readonly CommentFaker _faker = new();
|
|
||||||
private readonly CommentRepository _repository;
|
|
||||||
|
|
||||||
public CommentRepositoryTests()
|
|
||||||
{
|
|
||||||
_repository = new CommentRepository(_contextMock.Object, _loggerMock.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Upsert_WhenExceptionIsThrown_ItShouldLogError()
|
|
||||||
{
|
|
||||||
var entity = _faker.Generate();
|
|
||||||
var mockSet = new Mock<DbSet<Comment>>();
|
|
||||||
|
|
||||||
mockSet
|
|
||||||
.Setup(x => x.FindAsync(entity.Id))
|
|
||||||
.Throws<Exception>();
|
|
||||||
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.Set<Comment>())
|
|
||||||
.Returns(mockSet.Object);
|
|
||||||
|
|
||||||
await _repository.Upsert(entity);
|
|
||||||
|
|
||||||
_loggerMock.Verify(
|
|
||||||
x => x.Log(
|
|
||||||
LogLevel.Error,
|
|
||||||
It.IsAny<EventId>(),
|
|
||||||
It.IsAny<It.IsAnyType>(),
|
|
||||||
It.IsAny<Exception>(),
|
|
||||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Find_WhenExceptionIsThrown_ItShouldReturnEmptyListAndLogError()
|
|
||||||
{
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.Set<Comment>())
|
|
||||||
.Throws<Exception>();
|
|
||||||
|
|
||||||
var result = await _repository.Find(x => x.Id == 1);
|
|
||||||
|
|
||||||
result.Should().BeEmpty();
|
|
||||||
|
|
||||||
_loggerMock.Verify(
|
|
||||||
x => x.Log(
|
|
||||||
LogLevel.Error,
|
|
||||||
It.IsAny<EventId>(),
|
|
||||||
It.IsAny<It.IsAnyType>(),
|
|
||||||
It.IsAny<Exception>(),
|
|
||||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
namespace SanctionsSearch.Worker.Tests.Unit;
|
|
||||||
|
|
||||||
public class EfUnitOfWorkTests
|
|
||||||
{
|
|
||||||
private readonly Mock<DbContext> _contextMock = new();
|
|
||||||
private readonly Mock<ILogger<EfUnitOfWork>> _loggerMock = new();
|
|
||||||
private readonly Mock<ILoggerFactory> _loggerFactoryMock = new();
|
|
||||||
private readonly EfUnitOfWork _unitOfWork;
|
|
||||||
|
|
||||||
public EfUnitOfWorkTests()
|
|
||||||
{
|
|
||||||
_unitOfWork = new(_contextMock.Object, _loggerMock.Object, _loggerFactoryMock.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Sdns_WhenCalled_ShouldReturnSdnRepository()
|
|
||||||
{
|
|
||||||
_unitOfWork.Sdns.Should().BeOfType<SdnRepository>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Addresses_WhenCalled_ShouldReturnAddressRepository()
|
|
||||||
{
|
|
||||||
_unitOfWork.Addresses.Should().BeOfType<AddressRepository>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Aliases_WhenCalled_ShouldReturnAliasRepository()
|
|
||||||
{
|
|
||||||
_unitOfWork.Aliases.Should().BeOfType<AliasRepository>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Comments_WhenCalled_ShouldReturnCommentRepository()
|
|
||||||
{
|
|
||||||
_unitOfWork.Comments.Should().BeOfType<CommentRepository>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SaveChangesAsync_WhenADatabaseUpdateExceptionIsThrown_ItShouldBeCaughtAndLogged()
|
|
||||||
{
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
|
||||||
.ThrowsAsync(new DbUpdateException());
|
|
||||||
|
|
||||||
var action = _unitOfWork.SaveChangesAsync;
|
|
||||||
|
|
||||||
action.Should().NotThrowAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SaveChangesAsync_WhenExceptionIsThrown_ItShouldNotBeCaught()
|
|
||||||
{
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
|
||||||
.ThrowsAsync(new Exception());
|
|
||||||
|
|
||||||
var action = _unitOfWork.SaveChangesAsync;
|
|
||||||
|
|
||||||
action.Should().ThrowAsync<Exception>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DisposeAsync_WhenExceptionIsThrown_ItShouldBeCaught()
|
|
||||||
{
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.DisposeAsync())
|
|
||||||
.Throws(new Exception());
|
|
||||||
|
|
||||||
var action = async () => await _unitOfWork.DisposeAsync();
|
|
||||||
|
|
||||||
action.Should().NotThrowAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Dispose_WhenExceptionIsThrown_ItShouldBeCaught()
|
|
||||||
{
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.Dispose())
|
|
||||||
.Throws(new Exception());
|
|
||||||
|
|
||||||
var action = _unitOfWork.Dispose;
|
|
||||||
|
|
||||||
action.Should().NotThrow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
namespace SanctionsSearch.Worker.Tests.Unit;
|
|
||||||
|
|
||||||
public class SdnRepositoryTests
|
|
||||||
{
|
|
||||||
private readonly Mock<DbContext> _contextMock = new();
|
|
||||||
private readonly Mock<ILogger<SdnRepository>> _loggerMock = new();
|
|
||||||
private readonly SdnFaker _faker = new();
|
|
||||||
private readonly SdnRepository _repository;
|
|
||||||
|
|
||||||
public SdnRepositoryTests()
|
|
||||||
{
|
|
||||||
_repository = new SdnRepository(_contextMock.Object, _loggerMock.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Upsert_WhenExceptionIsThrown_ItShouldLogError()
|
|
||||||
{
|
|
||||||
var entity = _faker.Generate();
|
|
||||||
var mockSet = new Mock<DbSet<Sdn>>();
|
|
||||||
|
|
||||||
mockSet
|
|
||||||
.Setup(x => x.FindAsync(entity.Id))
|
|
||||||
.Throws<Exception>();
|
|
||||||
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.Set<Sdn>())
|
|
||||||
.Returns(mockSet.Object);
|
|
||||||
|
|
||||||
await _repository.Upsert(entity);
|
|
||||||
|
|
||||||
_loggerMock.Verify(
|
|
||||||
x => x.Log(
|
|
||||||
LogLevel.Error,
|
|
||||||
It.IsAny<EventId>(),
|
|
||||||
It.IsAny<It.IsAnyType>(),
|
|
||||||
It.IsAny<Exception>(),
|
|
||||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Find_WhenExceptionIsThrown_ItShouldReturnEmptyListAndLogError()
|
|
||||||
{
|
|
||||||
_contextMock
|
|
||||||
.Setup(x => x.Set<Sdn>())
|
|
||||||
.Throws<Exception>();
|
|
||||||
|
|
||||||
var result = await _repository.Find(x => x.Id == 1);
|
|
||||||
|
|
||||||
result.Should().BeEmpty();
|
|
||||||
|
|
||||||
_loggerMock.Verify(
|
|
||||||
x => x.Log(
|
|
||||||
LogLevel.Error,
|
|
||||||
It.IsAny<EventId>(),
|
|
||||||
It.IsAny<It.IsAnyType>(),
|
|
||||||
It.IsAny<Exception>(),
|
|
||||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace SanctionsSearch.Worker.Extensions;
|
||||||
|
|
||||||
|
static class SearchRequestExtensions
|
||||||
|
{
|
||||||
|
public static Expression<Func<Sdn, bool>> ToSdnFilter(this SearchRequest request)
|
||||||
|
{
|
||||||
|
var nameParts = request.Name.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
return sdn => nameParts.All(part => sdn.Name.Contains(part));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ namespace SanctionsSearch.Worker.Interfaces;
|
|||||||
|
|
||||||
interface IOnspringService
|
interface IOnspringService
|
||||||
{
|
{
|
||||||
|
Task<Result> UpdateSearchRequestAsProcessingAsync(SearchRequest request);
|
||||||
|
Task<Result> UpdateSearchRequestAsFailedAsync(SearchRequest request, string error);
|
||||||
Task<List<SearchRequest>> GetSearchRequestsAsync();
|
Task<List<SearchRequest>> GetSearchRequestsAsync();
|
||||||
Task<Result> AddSearchResultAsync(SearchResult result);
|
Task<Result> AddSearchResultAsync(SearchResult result);
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,33 @@ class Address : Entity
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{StreetAddress}, {CityProvincePostal}, {Country}";
|
var address = new StringBuilder();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(StreetAddress) is false)
|
||||||
|
{
|
||||||
|
address.Append(StreetAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(CityProvincePostal) is false)
|
||||||
|
{
|
||||||
|
if (address.Length > 0)
|
||||||
|
{
|
||||||
|
address.Append(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
address.Append(CityProvincePostal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Country) is false)
|
||||||
|
{
|
||||||
|
if (address.Length > 0)
|
||||||
|
{
|
||||||
|
address.Append(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
address.Append(Country);
|
||||||
|
}
|
||||||
|
|
||||||
|
return address.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ class Sdn : Entity
|
|||||||
Name = Name,
|
Name = Name,
|
||||||
Address = Addresses.FirstOrDefault()?.ToString() ?? string.Empty,
|
Address = Addresses.FirstOrDefault()?.ToString() ?? string.Empty,
|
||||||
Type = Type,
|
Type = Type,
|
||||||
Programs = [.. Program.Split("] [")]
|
Programs = [.. Program.Split("] [")],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,14 +4,4 @@ class SearchRequest
|
|||||||
{
|
{
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public string Address { get; set; } = string.Empty;
|
|
||||||
public string City { get; set; } = string.Empty;
|
|
||||||
public string State { get; set; } = string.Empty;
|
|
||||||
public string Zip { get; set; } = string.Empty;
|
|
||||||
public string Country { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public Expression<Func<Sdn, bool>> ToSdnFilter()
|
|
||||||
{
|
|
||||||
return sdn => EF.Functions.Like(sdn.Name, $"%{Name}%");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
namespace SanctionsSearch.Worker.Models;
|
namespace SanctionsSearch.Worker.Models;
|
||||||
|
|
||||||
class SearchResult
|
class SearchResult(int searchRequestId, List<Hit> hits)
|
||||||
{
|
{
|
||||||
public int SearchRequestId { get; init; }
|
public int SearchRequestId { get; init; } = searchRequestId;
|
||||||
public List<Hit> Hits { get; init; } = [];
|
public List<Hit> Hits { get; init; } = hits;
|
||||||
|
|
||||||
public SearchResult(int searchRequestId, List<Hit> hits)
|
|
||||||
{
|
|
||||||
SearchRequestId = searchRequestId;
|
|
||||||
Hits = hits;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Hit
|
class Hit
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ class SearchRequestOptions
|
|||||||
{
|
{
|
||||||
public int AppId { get; init; }
|
public int AppId { get; init; }
|
||||||
public int NameFieldId { get; init; }
|
public int NameFieldId { get; init; }
|
||||||
public int AddressFieldId { get; init; }
|
|
||||||
public int CityFieldId { get; init; }
|
|
||||||
public int StateFieldId { get; init; }
|
|
||||||
public int ZipFieldId { get; init; }
|
|
||||||
public int CountryFieldId { get; init; }
|
|
||||||
public int StatusFieldId { get; init; }
|
public int StatusFieldId { get; init; }
|
||||||
public Guid AwaitingProcessingStatusId { get; init; }
|
public Guid AwaitingProcessingStatusId { get; init; }
|
||||||
public Guid ProcessingStatusId { get; init; }
|
public Guid ProcessingStatusId { get; init; }
|
||||||
|
|||||||
@@ -13,41 +13,26 @@ class EfRepository<T> : IRepository<T> where T : Entity
|
|||||||
|
|
||||||
public async Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[]? includes)
|
public async Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[]? includes)
|
||||||
{
|
{
|
||||||
try
|
var query = _context.Set<T>().AsQueryable();
|
||||||
{
|
|
||||||
var query = _context.Set<T>().AsQueryable();
|
|
||||||
|
|
||||||
if (includes is not null)
|
if (includes is not null)
|
||||||
{
|
|
||||||
query = includes.Aggregate(query, (current, include) => current.Include(include));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query.Where(predicate).ToListAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error finding entities of type {Type}", typeof(T).Name);
|
query = includes.Aggregate(query, (current, include) => current.Include(include));
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await query.Where(predicate).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Upsert(T entity)
|
public async Task Upsert(T entity)
|
||||||
{
|
{
|
||||||
try
|
var existing = await _context.Set<T>().FindAsync(entity.Id);
|
||||||
{
|
|
||||||
var existing = await _context.Set<T>().FindAsync(entity.Id);
|
|
||||||
|
|
||||||
if (existing is null)
|
if (existing is null)
|
||||||
{
|
|
||||||
await _context.Set<T>().AddAsync(entity);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.Entry(existing).CurrentValues.SetValues(entity);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error upserting entity of type {Type} with {Id}", typeof(T).Name, entity.Id);
|
await _context.Set<T>().AddAsync(entity);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_context.Entry(existing).CurrentValues.SetValues(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,37 +15,16 @@ class EfUnitOfWork(
|
|||||||
|
|
||||||
public async Task SaveChangesAsync()
|
public async Task SaveChangesAsync()
|
||||||
{
|
{
|
||||||
try
|
await _context.SaveChangesAsync();
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
catch (DbUpdateException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to save changes to the database.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
try
|
await _context.DisposeAsync();
|
||||||
{
|
|
||||||
await _context.DisposeAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to dispose of the database context.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
try
|
_context.Dispose();
|
||||||
{
|
|
||||||
_context.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to dispose of the database context.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,7 @@ class Program
|
|||||||
|
|
||||||
builder.Services.AddScoped<IOfacFileService, OfacFileService>();
|
builder.Services.AddScoped<IOfacFileService, OfacFileService>();
|
||||||
builder.Services.AddScoped<IOnspringService, OnspringService>();
|
builder.Services.AddScoped<IOnspringService, OnspringService>();
|
||||||
|
builder.Services.AddScoped<ISearchService, SearchService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<ISdnRepository, SdnRepository>();
|
builder.Services.AddScoped<ISdnRepository, SdnRepository>();
|
||||||
builder.Services.AddScoped<IAddressRepository, AddressRepository>();
|
builder.Services.AddScoped<IAddressRepository, AddressRepository>();
|
||||||
@@ -91,6 +92,7 @@ class Program
|
|||||||
|
|
||||||
builder.Services.AddScoped<IDatabaseMaintainer, DatabaseMaintainer>();
|
builder.Services.AddScoped<IDatabaseMaintainer, DatabaseMaintainer>();
|
||||||
builder.Services.AddHostedService<DatabaseWorker>();
|
builder.Services.AddHostedService<DatabaseWorker>();
|
||||||
|
builder.Services.AddHostedService<OnspringWorker>();
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
namespace SanctionsSearch.Worker.Services;
|
namespace SanctionsSearch.Worker.Services;
|
||||||
|
|
||||||
class OnspringService(
|
class OnspringService(
|
||||||
@@ -11,14 +10,139 @@ class OnspringService(
|
|||||||
private readonly ILogger<OnspringService> _logger = logger;
|
private readonly ILogger<OnspringService> _logger = logger;
|
||||||
private readonly IOnspringClient _client = client;
|
private readonly IOnspringClient _client = client;
|
||||||
|
|
||||||
public Task<Result> AddSearchResultAsync(SearchResult result)
|
public async Task<Result> AddSearchResultAsync(SearchResult result)
|
||||||
{
|
{
|
||||||
// TODO: Implement this method
|
var typeValues = result.Hits.Select(h => h.Type).ToList();
|
||||||
// - we should add a new record for each hit
|
var typeFieldPairs = await GetOrAddListValuePairs(
|
||||||
// - each new hit record should reference the request
|
_options.SearchResultOptions.TypeFieldId,
|
||||||
// - we should update the request status to processed
|
typeValues
|
||||||
// - we should return a Result indicating success or failure
|
);
|
||||||
throw new NotImplementedException();
|
|
||||||
|
var programValues = result.Hits.SelectMany(h => h.Programs).ToList();
|
||||||
|
var programFieldPairs = await GetOrAddListValuePairs(
|
||||||
|
_options.SearchResultOptions.ProgramsFieldId,
|
||||||
|
programValues
|
||||||
|
);
|
||||||
|
|
||||||
|
var resultRecords = result.Hits.Select(hit =>
|
||||||
|
{
|
||||||
|
var resultRecord = new ResultRecord()
|
||||||
|
{
|
||||||
|
AppId = _options.SearchResultOptions.AppId,
|
||||||
|
FieldData = [
|
||||||
|
new IntegerFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchResultOptions.SearchRequestFieldId,
|
||||||
|
Value = result.SearchRequestId
|
||||||
|
},
|
||||||
|
new StringFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchResultOptions.NameFieldId,
|
||||||
|
Value = hit.Name
|
||||||
|
},
|
||||||
|
new StringFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchResultOptions.AddressFieldId,
|
||||||
|
Value = hit.Address
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(hit.Type) is false)
|
||||||
|
{
|
||||||
|
resultRecord.FieldData.Add(
|
||||||
|
new GuidFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchResultOptions.TypeFieldId,
|
||||||
|
Value = typeFieldPairs[hit.Type]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var programValues = hit.Programs
|
||||||
|
.Where(p => string.IsNullOrWhiteSpace(p) is false)
|
||||||
|
.Select(p => programFieldPairs[p])
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (programValues.Any())
|
||||||
|
{
|
||||||
|
resultRecord.FieldData.Add(
|
||||||
|
new GuidListFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchResultOptions.ProgramsFieldId,
|
||||||
|
Value = programValues
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultRecord;
|
||||||
|
});
|
||||||
|
|
||||||
|
var saveResultRequests = resultRecords.Select(_client.SaveRecordAsync);
|
||||||
|
var saveResultResponses = await Task.WhenAll(saveResultRequests);
|
||||||
|
|
||||||
|
foreach (var response in saveResultResponses)
|
||||||
|
{
|
||||||
|
if (response.IsSuccessful is false)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Failed to save search result: {StatusCode} - {Error}",
|
||||||
|
response.StatusCode,
|
||||||
|
response.Message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFailed = saveResultResponses.Any(r => r.IsSuccessful is false);
|
||||||
|
var updatedSearchRequest = isFailed
|
||||||
|
? new ResultRecord()
|
||||||
|
{
|
||||||
|
AppId = _options.SearchRequestOptions.AppId,
|
||||||
|
RecordId = result.SearchRequestId,
|
||||||
|
FieldData = [
|
||||||
|
new GuidFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchRequestOptions.StatusFieldId,
|
||||||
|
Value = _options.SearchRequestOptions.ProcessedErrorStatusId
|
||||||
|
},
|
||||||
|
new StringFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchRequestOptions.ErrorFieldId,
|
||||||
|
Value = "Unable to save all search results"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
: new ResultRecord()
|
||||||
|
{
|
||||||
|
AppId = _options.SearchRequestOptions.AppId,
|
||||||
|
RecordId = result.SearchRequestId,
|
||||||
|
FieldData = [
|
||||||
|
new GuidFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchRequestOptions.StatusFieldId,
|
||||||
|
Value = _options.SearchRequestOptions.ProcessedSuccessStatusId
|
||||||
|
},
|
||||||
|
new StringFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchRequestOptions.ErrorFieldId,
|
||||||
|
Value = string.Empty
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateRequestResponse = await _client.SaveRecordAsync(updatedSearchRequest);
|
||||||
|
|
||||||
|
if (updateRequestResponse.IsSuccessful is false)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Failed to update search request {RequestId} status: {StatusCode} - {Error}",
|
||||||
|
result.SearchRequestId,
|
||||||
|
updateRequestResponse.StatusCode,
|
||||||
|
updateRequestResponse.Message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFailed ? Result.Fail("Failed to saving all search results") : Result.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<SearchRequest>> GetSearchRequestsAsync()
|
public async Task<List<SearchRequest>> GetSearchRequestsAsync()
|
||||||
@@ -29,11 +153,6 @@ class OnspringService(
|
|||||||
Filter = $"{_options.SearchRequestOptions.StatusFieldId} contains '{_options.SearchRequestOptions.AwaitingProcessingStatusId}'",
|
Filter = $"{_options.SearchRequestOptions.StatusFieldId} contains '{_options.SearchRequestOptions.AwaitingProcessingStatusId}'",
|
||||||
FieldIds = [
|
FieldIds = [
|
||||||
_options.SearchRequestOptions.NameFieldId,
|
_options.SearchRequestOptions.NameFieldId,
|
||||||
_options.SearchRequestOptions.AddressFieldId,
|
|
||||||
_options.SearchRequestOptions.CityFieldId,
|
|
||||||
_options.SearchRequestOptions.StateFieldId,
|
|
||||||
_options.SearchRequestOptions.ZipFieldId,
|
|
||||||
_options.SearchRequestOptions.CountryFieldId
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,6 +208,135 @@ class OnspringService(
|
|||||||
return records.Select(MapRecordToSearchRequest).ToList();
|
return records.Select(MapRecordToSearchRequest).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Result> UpdateSearchRequestAsFailedAsync(SearchRequest request, string error)
|
||||||
|
{
|
||||||
|
var updatedSearchRequest = new ResultRecord()
|
||||||
|
{
|
||||||
|
AppId = _options.SearchRequestOptions.AppId,
|
||||||
|
RecordId = request.Id,
|
||||||
|
FieldData = [
|
||||||
|
new GuidFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchRequestOptions.StatusFieldId,
|
||||||
|
Value = _options.SearchRequestOptions.ProcessedErrorStatusId
|
||||||
|
},
|
||||||
|
new StringFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchRequestOptions.ErrorFieldId,
|
||||||
|
Value = error
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateRequestResponse = await _client.SaveRecordAsync(updatedSearchRequest);
|
||||||
|
|
||||||
|
if (updateRequestResponse.IsSuccessful is false)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Failed to update search request {RequestId} status: {StatusCode} - {Error}",
|
||||||
|
request.Id,
|
||||||
|
updateRequestResponse.StatusCode,
|
||||||
|
updateRequestResponse.Message
|
||||||
|
);
|
||||||
|
|
||||||
|
return Result.Fail("Failed to update search request status");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> UpdateSearchRequestAsProcessingAsync(SearchRequest request)
|
||||||
|
{
|
||||||
|
var updatedSearchRequest = new ResultRecord()
|
||||||
|
{
|
||||||
|
AppId = _options.SearchRequestOptions.AppId,
|
||||||
|
RecordId = request.Id,
|
||||||
|
FieldData = [
|
||||||
|
new GuidFieldValue()
|
||||||
|
{
|
||||||
|
FieldId = _options.SearchRequestOptions.StatusFieldId,
|
||||||
|
Value = _options.SearchRequestOptions.ProcessingStatusId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateRequestResponse = await _client.SaveRecordAsync(updatedSearchRequest);
|
||||||
|
|
||||||
|
if (updateRequestResponse.IsSuccessful is false)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Failed to update search request {RequestId} status: {StatusCode} - {Error}",
|
||||||
|
request.Id,
|
||||||
|
updateRequestResponse.StatusCode,
|
||||||
|
updateRequestResponse.Message
|
||||||
|
);
|
||||||
|
|
||||||
|
return Result.Fail("Failed to update search request status");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<string, Guid>> GetOrAddListValuePairs(int listFieldId, List<string> values)
|
||||||
|
{
|
||||||
|
var pairs = new Dictionary<string, Guid>();
|
||||||
|
|
||||||
|
var getFieldResponse = await _client.GetFieldAsync(listFieldId);
|
||||||
|
|
||||||
|
if (getFieldResponse.IsSuccessful is false)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Failed to retrieve field information for list field with id {ListFieldId}: {StatusCode} - {Error}",
|
||||||
|
listFieldId,
|
||||||
|
getFieldResponse.StatusCode,
|
||||||
|
getFieldResponse.Message
|
||||||
|
);
|
||||||
|
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getFieldResponse.Value is not ListField listField)
|
||||||
|
{
|
||||||
|
_logger.LogError("Field with id {ListFieldId} is not a list field", listFieldId);
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var value in values.Where(v => string.IsNullOrWhiteSpace(v) is false).Distinct())
|
||||||
|
{
|
||||||
|
var existingListFieldValue = listField.Values.FirstOrDefault(v => string.Equals(v.Name, value, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
|
if (existingListFieldValue is not null)
|
||||||
|
{
|
||||||
|
pairs.Add(value, existingListFieldValue.Id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveListFieldValueRequest = new SaveListItemRequest()
|
||||||
|
{
|
||||||
|
ListId = listField.ListId,
|
||||||
|
Name = value
|
||||||
|
};
|
||||||
|
|
||||||
|
var saveListFieldValueResponse = await _client.SaveListItemAsync(saveListFieldValueRequest);
|
||||||
|
|
||||||
|
if (saveListFieldValueResponse.IsSuccessful is false)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Failed to save list field value for list field with id {ListFieldId}: {StatusCode} - {Error}",
|
||||||
|
listFieldId,
|
||||||
|
saveListFieldValueResponse.StatusCode,
|
||||||
|
saveListFieldValueResponse.Message
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs.Add(value, saveListFieldValueResponse.Value.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
private SearchRequest MapRecordToSearchRequest(ResultRecord record)
|
private SearchRequest MapRecordToSearchRequest(ResultRecord record)
|
||||||
{
|
{
|
||||||
var searchRequest = new SearchRequest
|
var searchRequest = new SearchRequest
|
||||||
@@ -102,26 +350,6 @@ class OnspringService(
|
|||||||
{
|
{
|
||||||
searchRequest.Name = field.GetStringValue();
|
searchRequest.Name = field.GetStringValue();
|
||||||
}
|
}
|
||||||
else if (field.FieldId == _options.SearchRequestOptions.AddressFieldId)
|
|
||||||
{
|
|
||||||
searchRequest.Address = field.GetStringValue();
|
|
||||||
}
|
|
||||||
else if (field.FieldId == _options.SearchRequestOptions.CityFieldId)
|
|
||||||
{
|
|
||||||
searchRequest.City = field.GetStringValue();
|
|
||||||
}
|
|
||||||
else if (field.FieldId == _options.SearchRequestOptions.StateFieldId)
|
|
||||||
{
|
|
||||||
searchRequest.State = field.GetStringValue();
|
|
||||||
}
|
|
||||||
else if (field.FieldId == _options.SearchRequestOptions.ZipFieldId)
|
|
||||||
{
|
|
||||||
searchRequest.Zip = field.GetStringValue();
|
|
||||||
}
|
|
||||||
else if (field.FieldId == _options.SearchRequestOptions.CountryFieldId)
|
|
||||||
{
|
|
||||||
searchRequest.Country = field.GetStringValue();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return searchRequest;
|
return searchRequest;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ global using System.Collections.Concurrent;
|
|||||||
global using System.Globalization;
|
global using System.Globalization;
|
||||||
global using System.Linq.Expressions;
|
global using System.Linq.Expressions;
|
||||||
global using System.Reflection;
|
global using System.Reflection;
|
||||||
|
global using System.Text;
|
||||||
|
|
||||||
global using CsvHelper;
|
global using CsvHelper;
|
||||||
global using CsvHelper.Configuration;
|
global using CsvHelper.Configuration;
|
||||||
|
|||||||
@@ -42,10 +42,62 @@ public class OnspringWorker(
|
|||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RunSearchRequests()
|
private async Task RunSearchRequests()
|
||||||
{
|
{
|
||||||
|
var searchBatchIdProperty = LogContext.PushProperty("SearchBatchId", Guid.NewGuid());
|
||||||
|
|
||||||
_logger.LogInformation("Running search requests");
|
_logger.LogInformation("Running search requests");
|
||||||
|
|
||||||
return Task.CompletedTask;
|
try
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateAsyncScope();
|
||||||
|
var onspringService = scope.ServiceProvider.GetRequiredService<IOnspringService>();
|
||||||
|
var searchService = scope.ServiceProvider.GetRequiredService<ISearchService>();
|
||||||
|
|
||||||
|
var searchRequests = await onspringService.GetSearchRequestsAsync();
|
||||||
|
|
||||||
|
if (searchRequests.Count is 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No search requests found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var searchRequest in searchRequests)
|
||||||
|
{
|
||||||
|
var searchBatchItemIdProperty = LogContext.PushProperty("SearchBatchItemId", Guid.NewGuid());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Processing search request {SearchRequestId}", searchRequest.Id);
|
||||||
|
|
||||||
|
await onspringService.UpdateSearchRequestAsProcessingAsync(searchRequest);
|
||||||
|
var searchResult = await searchService.PerformSearchAsync(searchRequest);
|
||||||
|
await onspringService.AddSearchResultAsync(searchResult);
|
||||||
|
|
||||||
|
_logger.LogInformation("Search request {SearchRequestId} processed", searchRequest.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await onspringService.UpdateSearchRequestAsFailedAsync(
|
||||||
|
searchRequest,
|
||||||
|
$"Failed to process search request: {ex.Message}"
|
||||||
|
);
|
||||||
|
|
||||||
|
_logger.LogError(ex, "Failed to process search request {SearchRequestId}", searchRequest.Id);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
searchBatchItemIdProperty.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to run search requests");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
searchBatchIdProperty.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,11 +27,6 @@
|
|||||||
"SearchRequestOptions": {
|
"SearchRequestOptions": {
|
||||||
"AppId": 949,
|
"AppId": 949,
|
||||||
"NameFieldId": 20946,
|
"NameFieldId": 20946,
|
||||||
"AddressFieldId": 20947,
|
|
||||||
"CityFieldId": 20948,
|
|
||||||
"StateFieldId": 20949,
|
|
||||||
"ZipFieldId": 20967,
|
|
||||||
"CountryFieldId": 20950,
|
|
||||||
"StatusFieldId": 20964,
|
"StatusFieldId": 20964,
|
||||||
"AwaitingProcessingStatusId": "3cea19f0-afae-4dac-ac5b-e6f5555659b3",
|
"AwaitingProcessingStatusId": "3cea19f0-afae-4dac-ac5b-e6f5555659b3",
|
||||||
"ProcessingStatusId": "597c3096-ab0e-4910-a7a9-98d149acee81",
|
"ProcessingStatusId": "597c3096-ab0e-4910-a7a9-98d149acee81",
|
||||||
|
|||||||
Reference in New Issue
Block a user