From d4ce3b25773cc7facd729b206ac7a0c3d11e5532 Mon Sep 17 00:00:00 2001 From: Jose Henrique Date: Fri, 12 Sep 2025 21:18:36 -0300 Subject: [PATCH] issues #36 #37 and #44 --- OpenCand.API/Config/RateLimitingConfig.cs | 5 +++ .../Controllers/CandidatoController.cs | 2 +- .../Controllers/EstatisticaController.cs | 4 +- .../Repository/CandidatoRepository.cs | 10 +++++ .../Repository/EstatisticaRepository.cs | 38 ++++++++++++++-- OpenCand.API/Services/CachePreloadService.cs | 3 +- OpenCand.API/Services/EstatisticaService.cs | 36 +++++++-------- OpenCand.API/Services/OpenCandService.cs | 45 ++++++++++++++++++- OpenCand.API/appsettings.json | 4 +- 9 files changed, 118 insertions(+), 29 deletions(-) diff --git a/OpenCand.API/Config/RateLimitingConfig.cs b/OpenCand.API/Config/RateLimitingConfig.cs index 9bd21b4..261633f 100644 --- a/OpenCand.API/Config/RateLimitingConfig.cs +++ b/OpenCand.API/Config/RateLimitingConfig.cs @@ -51,6 +51,11 @@ namespace OpenCand.API.Config options.OnRejected = async (context, token) => { + var loggerFactory = context.HttpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger("RateLimitingConfig"); + var clientIdentifier = GetClientIdentifier(context.HttpContext); + logger.LogWarning("Rate limit exceeded for client {ClientIdentifier}", clientIdentifier); + context.HttpContext.Response.StatusCode = 429; var retryAfter = GetRetryAfter(context); diff --git a/OpenCand.API/Controllers/CandidatoController.cs b/OpenCand.API/Controllers/CandidatoController.cs index 00b3ef0..aa44178 100644 --- a/OpenCand.API/Controllers/CandidatoController.cs +++ b/OpenCand.API/Controllers/CandidatoController.cs @@ -21,7 +21,7 @@ namespace OpenCand.API.Controllers [EnableRateLimiting(RateLimitingConfig.CandidatoSearchPolicy)] public async Task CandidatoSearch([FromQuery] string q) { - if (string.IsNullOrEmpty(q) || q.Length == 1) + if (string.IsNullOrEmpty(q) || q.Length < 3) { throw new ArgumentException("Query parameter 'q' cannot be null/empty.", nameof(q)); } diff --git a/OpenCand.API/Controllers/EstatisticaController.cs b/OpenCand.API/Controllers/EstatisticaController.cs index 3c431b4..2aec843 100644 --- a/OpenCand.API/Controllers/EstatisticaController.cs +++ b/OpenCand.API/Controllers/EstatisticaController.cs @@ -21,9 +21,9 @@ namespace OpenCand.API.Controllers } [HttpGet("enriquecimento")] - public async Task> GetMaioresEnriquecimentos() + public async Task> GetMaioresEnriquecimentos([FromQuery] GetValueSumRequestFilter requestFilter) { - return await estatisticaService.GetMaioresEnriquecimentos(); + return await estatisticaService.GetMaioresEnriquecimentos(requestFilter); } [HttpPost("values-sum")] diff --git a/OpenCand.API/Repository/CandidatoRepository.cs b/OpenCand.API/Repository/CandidatoRepository.cs index 0951b95..90274d1 100644 --- a/OpenCand.API/Repository/CandidatoRepository.cs +++ b/OpenCand.API/Repository/CandidatoRepository.cs @@ -102,6 +102,16 @@ namespace OpenCand.Repository } } + public async Task IncreaseCandidatoPopularity(Guid idcandidato) + { + using var connection = new NpgsqlConnection(ConnectionString); + await connection.ExecuteAsync(@" + UPDATE candidato + SET popularidade = popularidade + 1 + WHERE idcandidato = @idcandidato; + ", new { idcandidato }); + } + public async Task GetRandomCandidatoIdAsync() { using var connection = new NpgsqlConnection(ConnectionString); diff --git a/OpenCand.API/Repository/EstatisticaRepository.cs b/OpenCand.API/Repository/EstatisticaRepository.cs index 0f337d8..4306bdd 100644 --- a/OpenCand.API/Repository/EstatisticaRepository.cs +++ b/OpenCand.API/Repository/EstatisticaRepository.cs @@ -2,8 +2,9 @@ using Microsoft.Extensions.Caching.Memory; using Npgsql; using OpenCand.API.Model; -using OpenCand.Core.Models; using OpenCand.Repository; +using System.Text.Json; +using static OpenCand.API.Model.GetValueSumRequest; namespace OpenCand.API.Repository { @@ -16,9 +17,39 @@ namespace OpenCand.API.Repository this.configuration = configuration; } - public async Task> GetMaioresEnriquecimentos() + public async Task> GetMaioresEnriquecimentos(GetValueSumRequestFilter? requestFilter = null) { - string cacheKey = GenerateCacheKey("GetMaioresEnriquecimento"); + var joinBase = string.Empty; + if (requestFilter == null) requestFilter = new GetValueSumRequestFilter(); + else + { + joinBase = " JOIN candidato_mapping cm ON ed.idcandidato = cm.idcandidato "; + var whereConditions = new List(); + if (!string.IsNullOrWhiteSpace(requestFilter.Partido)) + { + whereConditions.Add($"cm.sgpartido = '{requestFilter.Partido}'"); + } + if (!string.IsNullOrWhiteSpace(requestFilter.Uf)) + { + whereConditions.Add($"cm.siglauf = '{requestFilter.Uf.ToUpper()}'"); + } + if (requestFilter.Ano != null) + { + whereConditions.Add($"cm.ano = '{requestFilter.Ano}'"); + } + if (!string.IsNullOrEmpty(requestFilter.Cargo)) + { + whereConditions.Add($"cm.cargo = '{requestFilter.Cargo}'"); + } + if (whereConditions.Count > 0) + { + joinBase += " WHERE " + string.Join(" AND ", whereConditions); + } + } + + var requestJson = JsonSerializer.Serialize(requestFilter); + + string cacheKey = GenerateCacheKey("GetMaioresEnriquecimento", requestJson); var result = await GetOrSetCacheAsync(cacheKey, async () => { @@ -46,6 +77,7 @@ namespace OpenCand.API.Repository JOIN candidato c ON ed.idcandidato = c.idcandidato JOIN mv_bem_candidato pi ON ed.idcandidato = pi.idcandidato AND ed.anoInicial = pi.ano JOIN mv_bem_candidato pf ON ed.idcandidato = pf.idcandidato AND ed.anoFinal = pf.ano + " + joinBase + @" ORDER BY enriquecimento DESC LIMIT 25;") diff --git a/OpenCand.API/Services/CachePreloadService.cs b/OpenCand.API/Services/CachePreloadService.cs index b5c3da5..c12fee3 100644 --- a/OpenCand.API/Services/CachePreloadService.cs +++ b/OpenCand.API/Services/CachePreloadService.cs @@ -58,13 +58,14 @@ namespace OpenCand.API.Services await PerformPreLoad("GetValueSum", () => estatisticaService.GetValueSum(request)); } } + + await PerformPreLoad("GetMaioresEnriquecimentos", () => estatisticaService.GetMaioresEnriquecimentos(null)); } private async Task PreloadSingleEndpoints(EstatisticaService estatisticaService, OpenCandService openCandService) { logger.LogInformation("Preloading single-call endpoints..."); - await PerformPreLoad("GetOpenCandStatsAsync", estatisticaService.GetMaioresEnriquecimentos); await PerformPreLoad("GetOpenCandStatsAsync", openCandService.GetOpenCandStatsAsync); await PerformPreLoad("GetDatabaseTechStatsAsync", openCandService.GetDatabaseTechStatsAsync); await PerformPreLoad("GetDataAvailabilityStatsAsync", openCandService.GetDataAvailabilityStatsAsync); diff --git a/OpenCand.API/Services/EstatisticaService.cs b/OpenCand.API/Services/EstatisticaService.cs index 98b00db..91876f2 100644 --- a/OpenCand.API/Services/EstatisticaService.cs +++ b/OpenCand.API/Services/EstatisticaService.cs @@ -1,56 +1,54 @@ using OpenCand.API.Model; using OpenCand.API.Repository; -using OpenCand.Repository; +using static OpenCand.API.Model.GetValueSumRequest; namespace OpenCand.API.Services { public class EstatisticaService { - private readonly OpenCandRepository openCandRepository; - private readonly CandidatoRepository candidatoRepository; - private readonly BemCandidatoRepository bemCandidatoRepository; - private readonly DespesaReceitaRepository despesaReceitaRepository; private readonly EstatisticaRepository estatisticaRepository; - private readonly IConfiguration configuration; private readonly ILogger logger; public EstatisticaService( - OpenCandRepository openCandRepository, - CandidatoRepository candidatoRepository, - BemCandidatoRepository bemCandidatoRepository, - DespesaReceitaRepository despesaReceitaRepository, EstatisticaRepository estatisticaRepository, - IConfiguration configuration, ILogger logger) { - this.openCandRepository = openCandRepository; - this.candidatoRepository = candidatoRepository; - this.bemCandidatoRepository = bemCandidatoRepository; - this.despesaReceitaRepository = despesaReceitaRepository; this.estatisticaRepository = estatisticaRepository; - this.configuration = configuration; this.logger = logger; } public async Task GetConfigurationModel() { + logger.LogInformation("Getting configuration model"); + return await estatisticaRepository.GetConfiguration(); } - public async Task> GetMaioresEnriquecimentos() + public async Task> GetMaioresEnriquecimentos(GetValueSumRequestFilter? requestFilter = null) { - return await estatisticaRepository.GetMaioresEnriquecimentos(); + logger.LogInformation($"Getting maiores enriquecimentos. Filters: Partido={requestFilter?.Partido}, Uf={requestFilter?.Uf}, Ano={requestFilter?.Ano}, Cargo={requestFilter?.Cargo}"); + + return await estatisticaRepository.GetMaioresEnriquecimentos(requestFilter); } public async Task> GetValueSum(GetValueSumRequest request) { + logger.LogInformation($"Getting value sum for {request.Type} grouped by {request.GroupBy}. Filters: Partido={request.Filter?.Partido}, Uf={request.Filter?.Uf}, Ano={request.Filter?.Ano}, Cargo={request.Filter?.Cargo}"); + // count exec time + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + ValidateRequest(request); var sqlBuilder = new SqlQueryBuilder(); var query = sqlBuilder.BuildQuery(request); var parameters = sqlBuilder.GetParameters(); - return await estatisticaRepository.GetValueSum(query, parameters); + var result = await estatisticaRepository.GetValueSum(query, parameters); + + stopwatch.Stop(); + logger.LogInformation($"GetValueSum - Execution time: {stopwatch.ElapsedMilliseconds} ms"); + + return result; } private void ValidateRequest(GetValueSumRequest request) diff --git a/OpenCand.API/Services/OpenCandService.cs b/OpenCand.API/Services/OpenCandService.cs index f504ee6..c894701 100644 --- a/OpenCand.API/Services/OpenCandService.cs +++ b/OpenCand.API/Services/OpenCandService.cs @@ -37,11 +37,15 @@ namespace OpenCand.API.Services public async Task GetOpenCandStatsAsync() { + logger.LogInformation("Getting OpenCand stats"); + return await openCandRepository.GetOpenCandStatsAsync(); } public async Task GetDataAvailabilityStatsAsync() { + logger.LogInformation("Getting data availability stats"); + var stats = await openCandRepository.GetDataAvailabilityAsync(); return stats; @@ -49,6 +53,8 @@ namespace OpenCand.API.Services public async Task GetDatabaseTechStatsAsync() { + logger.LogInformation("Getting database tech stats"); + var stats = await openCandRepository.GetDatabaseTechStatsAsync(); stats.Tables = stats.Tables.OrderBy(t => t.Name).ToList(); @@ -59,14 +65,18 @@ namespace OpenCand.API.Services public async Task SearchCandidatosAsync(string query) { + logger.LogInformation($"Searching candidatos with query: {query}"); + return new CandidatoSearchResult() { - Candidatos = await candidatoRepository.SearchCandidatosAsync(query) + Candidatos = await candidatoRepository.SearchCandidatosAsync(query) ?? new List() }; } public async Task GetRandomCandidato() { + logger.LogInformation("Getting random candidato"); + return await candidatoRepository.GetRandomCandidatoIdAsync(); } @@ -76,6 +86,15 @@ namespace OpenCand.API.Services var eleicoes = await candidatoRepository.GetCandidatoMappingById(idcandidato); var candidatoExt = await candidatoRepository.GetCandidatoExtById(idcandidato); + try + { + await candidatoRepository.IncreaseCandidatoPopularity(idcandidato); + } + catch (Exception ex) + { + logger.LogError(ex, $"Error increasing popularity for Candidato ID {idcandidato}"); + } + if (result == null) { throw new KeyNotFoundException($"Candidato with ID {idcandidato} not found."); @@ -84,6 +103,10 @@ namespace OpenCand.API.Services { throw new KeyNotFoundException($"CandidatoMapping for ID {idcandidato} not found."); } + if (candidatoExt == null) + { + throw new KeyNotFoundException($"CandidatoExt for ID {idcandidato} not found."); + } foreach (var eleicao in eleicoes) { @@ -96,6 +119,8 @@ namespace OpenCand.API.Services result.Eleicoes = eleicoes.OrderByDescending(e => e.Ano).ToList(); result.CandidatoExt = candidatoExt.OrderByDescending(ce => ce.Ano).ToList(); + logger.LogDebug($"Found Candidato: {result.Nome}, ID: {result.IdCandidato}, CPF: {result.Cpf}, FotoUrl: {result.FotoUrl}"); + return result; } @@ -107,6 +132,8 @@ namespace OpenCand.API.Services result = new List(); } + logger.LogInformation($"Found {result.Count} bens for Candidato ID {idcandidato}"); + return new BemCandidatoResult() { Bens = result.OrderByDescending(r => r.TipoBem).ThenByDescending(r => r.Valor).ToList() @@ -115,12 +142,16 @@ namespace OpenCand.API.Services public async Task GetCandidatoRedeSocialById(Guid idcandidato) { + logger.LogInformation($"Getting redes sociais for Candidato ID {idcandidato}"); + var result = await candidatoRepository.GetCandidatoRedeSocialById(idcandidato); if (result == null) { result = new List(); } + logger.LogDebug($"Found {result.Count} redes sociais for Candidato ID {idcandidato}"); + return new RedeSocialResult() { RedesSociais = result.OrderByDescending(r => r.Ano).ToList() @@ -129,12 +160,16 @@ namespace OpenCand.API.Services public async Task GetCandidatoCpfById(Guid idcandidato) { + logger.LogInformation($"Getting CPF for Candidato ID {idcandidato}"); + var result = await candidatoRepository.GetCandidatoCpfAsync(idcandidato); if (result == null) { return new CpfRevealResult(); } + logger.LogDebug($"Found CPF {result} for Candidato ID {idcandidato}"); + return new CpfRevealResult() { Cpf = result @@ -143,12 +178,16 @@ namespace OpenCand.API.Services public async Task GetDespesasByIdAndYear(Guid idcandidato) { + logger.LogInformation($"Getting despesas for Candidato ID {idcandidato}"); + var result = await despesaReceitaRepository.GetDespesasByCandidatoIdYearAsync(idcandidato); if (result == null) { return new DespesasResult(); } + logger.LogDebug($"Found {result.Count} despesas for Candidato ID {idcandidato}"); + return new DespesasResult() { Despesas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList() @@ -157,12 +196,16 @@ namespace OpenCand.API.Services public async Task GetReceitasByIdAndYear(Guid idcandidato) { + logger.LogInformation($"Getting receitas for Candidato ID {idcandidato}"); + var result = await despesaReceitaRepository.GetReceitasByCandidatoIdYearAsync(idcandidato); if (result == null) { return new ReceitaResult(); } + logger.LogDebug($"Found {result.Count} receitas for Candidato ID {idcandidato}"); + return new ReceitaResult() { Receitas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList() diff --git a/OpenCand.API/appsettings.json b/OpenCand.API/appsettings.json index 99ac803..e915a04 100644 --- a/OpenCand.API/appsettings.json +++ b/OpenCand.API/appsettings.json @@ -1,8 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Debug", + "Microsoft.AspNetCore": "Information" } }, "DatabaseSettings": {