diff --git a/DataImport.API/Controllers/CountryController.cs b/DataImport.API/Controllers/CountryController.cs index 20a2a65..9a30ad9 100644 --- a/DataImport.API/Controllers/CountryController.cs +++ b/DataImport.API/Controllers/CountryController.cs @@ -1,9 +1,11 @@ using DataImport.API.Controllers.BaseController; using DataImport.Commands.Queries; -using DataImport.Data.Enums; // <-- add this +using DataImport.Data.Enums; +using DataImport.API.Filters; namespace DataImport.API.Controllers; +[ValidateEnums] [Route("api/v{version:apiVersion}/[controller]")] public class CountryController : ApiControllerBasev1 { @@ -14,33 +16,51 @@ public CountryController(IMediator mediator) _mediator = mediator; } - [HttpPost] - [Route("get-count-by-country")] + [HttpGet("get-count-by-country")] public async Task GetCountryCount( [FromQuery] Country country, CancellationToken ct) { - if (!Enum.IsDefined(typeof(Country), country)) - return BadRequest("Valid country is required"); - var result = await _mediator.Send(new GetCountryCountQuery(country), ct); return Ok(result); } - [HttpPost] - [Route("get-sanctions-by-country")] + [HttpGet("get-delisted-count-by-country")] + public async Task GetDelistedCountByCountry( + [FromQuery] Country country, + CancellationToken ct) + { + var result = await _mediator.Send(new GetCountryDelistedCountQuery(country), ct); + return Ok(result); + } + + [HttpGet("get-sanctions-by-country")] public async Task GetCountrySanctionsPaged( [FromQuery] Country country, [FromQuery] int pageSize = 20, [FromQuery] int page = 1, CancellationToken ct = default) { - if (!Enum.IsDefined(typeof(Country), country)) - return BadRequest("Valid country is required"); + pageSize = Math.Clamp(pageSize, 1, 100); + page = Math.Max(page, 1); + + var result = await _mediator.Send( + new GetCountrySanctionsPagedQuery(country, pageSize, page), ct); + return Ok(result); + } + + [HttpGet("get-delisted-sanctions-by-country")] + public async Task GetDelistedCountrySanctionsPaged( + [FromQuery] Country country, + [FromQuery] int pageSize = 20, + [FromQuery] int page = 1, + CancellationToken ct = default) + { + pageSize = Math.Clamp(pageSize, 1, 100); + page = Math.Max(page, 1); var result = await _mediator.Send( - new GetCountrySanctionsPagedQuery(country, pageSize, page), - ct); + new GetDelistedCountrySanctionsPagedQuery(country, pageSize, page), ct); return Ok(result); } diff --git a/DataImport.API/Controllers/SanctionsController.cs b/DataImport.API/Controllers/SanctionsController.cs index 3ce28e9..3573e9e 100644 --- a/DataImport.API/Controllers/SanctionsController.cs +++ b/DataImport.API/Controllers/SanctionsController.cs @@ -33,5 +33,20 @@ public async Task GetPaged( new GetSanctionsPagedQuery(page, pageSize, sdnType, lastNameContains), ct); return Ok(result); } + + [HttpGet("Paged-delisted-Sanctions")] + public async Task GetDelistedPaged( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? sdnType = null, + [FromQuery] string? lastNameContains = null, + CancellationToken ct = default) + { + var result = await _mediator.Send( + new GetDelistedSanctionsPagedQuery(page, pageSize, sdnType, lastNameContains), ct); + return Ok(result); + } + + } } diff --git a/DataImport.API/Filters/ValidateEnumsAttribute.cs b/DataImport.API/Filters/ValidateEnumsAttribute.cs new file mode 100644 index 0000000..b122dff --- /dev/null +++ b/DataImport.API/Filters/ValidateEnumsAttribute.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace DataImport.API.Filters; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public sealed class ValidateEnumsAttribute : ActionFilterAttribute +{ + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + context.Result = new BadRequestObjectResult( + new ValidationProblemDetails(context.ModelState)); + return; + } + + foreach (var (name, value) in context.ActionArguments) + { + if (value is null) continue; + + var type = value.GetType(); + if (type.IsEnum && !Enum.IsDefined(type, value)) + { + context.ModelState.AddModelError(name, $"'{value}' is not a valid {type.Name}."); + context.Result = new BadRequestObjectResult( + new ValidationProblemDetails(context.ModelState)); + return; + } + } + } +} \ No newline at end of file diff --git a/DataImport.Commands/Commands/GetCountryCountQueryCommand.cs b/DataImport.Commands/Commands/GetCountryCountQueryCommand.cs index 477c29b..5d04791 100644 --- a/DataImport.Commands/Commands/GetCountryCountQueryCommand.cs +++ b/DataImport.Commands/Commands/GetCountryCountQueryCommand.cs @@ -6,7 +6,7 @@ namespace DataImport.Commands.Commands; -public record CountryCountResult(bool Success, String Country, int Count); +public record CountryCountResult(bool Success, String Country, int Count, bool isDelisted = false); public class GetCountryCountQueryCommand : IRequestHandler { diff --git a/DataImport.Commands/Commands/GetCountryDelistedCountQueryCommand.cs b/DataImport.Commands/Commands/GetCountryDelistedCountQueryCommand.cs new file mode 100644 index 0000000..7139ab7 --- /dev/null +++ b/DataImport.Commands/Commands/GetCountryDelistedCountQueryCommand.cs @@ -0,0 +1,42 @@ +using DataImport.Commands.Queries; +using DataImport.Data.Data; +using MediatR; +using Microsoft.EntityFrameworkCore; +using ZiggyCreatures.Caching.Fusion; + +namespace DataImport.Commands.Commands; + +public class GetCountryDelistedCountQueryCommand + : IRequestHandler +{ + private readonly SanctionsDbContext _db; + private readonly IFusionCache _cache; + + public GetCountryDelistedCountQueryCommand(SanctionsDbContext db, IFusionCache cache) + { + _db = db; + _cache = cache; + } + + public async Task Handle( + GetCountryDelistedCountQuery request, + CancellationToken ct) + { + var country = request.country.ToString(); + var cacheKey = $"CountryDelistedCount_{country}"; + + return await _cache.GetOrSetAsync( + cacheKey, + async _ => + { + var count = await _db.SanctionDetails + .Where(x => x.Country == country && !x.IsActive) + .CountAsync(ct); + + return new CountryCountResult(true, country, count, isDelisted: true); + }, + options => options.SetDuration(TimeSpan.FromMinutes(5)), + ct + ); + } +} \ No newline at end of file diff --git a/DataImport.Commands/Commands/GetDelistedCountrySanctionsPagedQueryCommand.cs b/DataImport.Commands/Commands/GetDelistedCountrySanctionsPagedQueryCommand.cs new file mode 100644 index 0000000..2a77a06 --- /dev/null +++ b/DataImport.Commands/Commands/GetDelistedCountrySanctionsPagedQueryCommand.cs @@ -0,0 +1,60 @@ +using DataImport.Data.Data; +using DataImport.Presentation.GenericDTO; +using MediatR; +using Microsoft.EntityFrameworkCore; +using ZiggyCreatures.Caching.Fusion; +using Facet.Extensions; +using DataImport.Commands.Queries; + +namespace DataImport.Commands.Commands; + +public class GetDelistedCountrySanctionsPagedQueryHandler + : IRequestHandler> +{ + private readonly SanctionsDbContext _db; + private readonly IFusionCache _cache; + + public GetDelistedCountrySanctionsPagedQueryHandler(SanctionsDbContext db, IFusionCache cache) + { + _db = db; + _cache = cache; + } + + public async Task> Handle( + GetDelistedCountrySanctionsPagedQuery request, CancellationToken ct) + { + var countryName = request.country.ToString(); + + // "Delisted" prefix keeps this from colliding with the active-sanctions cache key + var cacheKey = $"DelistedCountrySanctionsPaged_{countryName}_{request.Page}_{request.PageSize}"; + + return await _cache.GetOrSetAsync>( + cacheKey, + async _ => + { + var query = _db.SanctionDetails + .AsNoTracking() + .Where(x => x.Country == countryName && !x.IsActive); // adjust to your delist field + + var total = await query.CountAsync(ct); + + var items = await query + .OrderBy(x => x.Id) + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .Select(x => x.ToFacet()) + .ToListAsync(ct); + + return new PagedResult( + items, + request.Page, + request.PageSize, + total, + (int)Math.Ceiling(total / (double)request.PageSize) + ); + }, + options => options.SetDuration(TimeSpan.FromMinutes(5)), + ct + ); + } +} \ No newline at end of file diff --git a/DataImport.Commands/Commands/GetDelistedSanctionsPagedQueryCommand.cs b/DataImport.Commands/Commands/GetDelistedSanctionsPagedQueryCommand.cs new file mode 100644 index 0000000..f18a13e --- /dev/null +++ b/DataImport.Commands/Commands/GetDelistedSanctionsPagedQueryCommand.cs @@ -0,0 +1,64 @@ +using DataImport.Data.Data; +using DataImport.Presentation.GenericDTO; +using Facet.Extensions; +using MediatR; +using Microsoft.EntityFrameworkCore; +using DataImport.Commands.Queries; +using ZiggyCreatures.Caching.Fusion; + +namespace DataImport.Commands.Commands; + +public class GetDelistedSanctionsPagedQueryCommand + : IRequestHandler> +{ + private readonly SanctionsDbContext _db; + private readonly IFusionCache _cache; + + + public GetDelistedSanctionsPagedQueryCommand(SanctionsDbContext db, IFusionCache cache) + { + _db = db; + _cache = cache; + } + + public async Task> Handle( + GetDelistedSanctionsPagedQuery request, CancellationToken ct) + { + var cacheKey = + $"DelistedSanctionsPaged_{request.SdnType}_{request.LastNameContains}_{request.Page}_{request.PageSize}"; + + return await _cache.GetOrSetAsync>( + cacheKey, + async _ => + { + var query = _db.SanctionDetails + .AsNoTracking() + .Where(s => !s.IsActive); // apply once, before count + + if (!string.IsNullOrWhiteSpace(request.SdnType)) + query = query.Where(s => s.SdnType == request.SdnType); + + if (!string.IsNullOrWhiteSpace(request.LastNameContains)) + query = query.Where(s => s.LastName.Contains(request.LastNameContains)); + + var total = await query.CountAsync(ct); + + var items = await query + .OrderBy(s => s.LastName) + .ThenBy(s => s.Id) // tiebreaker, see below + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .Select(s => s.ToFacet()) + .ToListAsync(ct); + + return new PagedResult( + items, + request.Page, + request.PageSize, + total, + (int)Math.Ceiling(total / (double)request.PageSize)); + }, + options => options.SetDuration(TimeSpan.FromMinutes(5)), + ct); + } +} \ No newline at end of file diff --git a/DataImport.Commands/Queries/CountryQueries.cs b/DataImport.Commands/Queries/CountryQueries.cs index 8adedb0..3889daa 100644 --- a/DataImport.Commands/Queries/CountryQueries.cs +++ b/DataImport.Commands/Queries/CountryQueries.cs @@ -14,5 +14,14 @@ public record GetCountrySanctionsPagedQuery( int Page = 1 ) : IRequest>; + +public record GetDelistedCountrySanctionsPagedQuery(Country country, + int PageSize, + int Page) + : IRequest>; + + public record GetCountryCountQuery(Country country) : IRequest; +public record GetCountryDelistedCountQuery(Country country) : IRequest; + diff --git a/DataImport.Commands/Queries/SanctionQueries.cs b/DataImport.Commands/Queries/SanctionQueries.cs index f82948c..9192dfa 100644 --- a/DataImport.Commands/Queries/SanctionQueries.cs +++ b/DataImport.Commands/Queries/SanctionQueries.cs @@ -13,6 +13,14 @@ public record GetSanctionsPagedQuery( string? LastNameContains = null ) : IRequest>; +public record GetDelistedSanctionsPagedQuery( + int Page = 1, + int PageSize = 20, + string? SdnType = null, + string? LastNameContains = null +) : IRequest>; + + public record SearchRequest(string Name); // Added a normal Tolerance as it is not optimized yet.