Compare commits

...

25 Commits

Author SHA1 Message Date
579517a1d4 improving db connection
All checks were successful
API and ETL Build / build_api (push) Successful in 45s
API and ETL Build / build_etl (push) Successful in 1m11s
2025-09-12 21:36:44 -03:00
d4ce3b2577 issues #36 #37 and #44 2025-09-12 21:18:36 -03:00
a1440baf3d #39 tweaking API rates
All checks were successful
API and ETL Build / build_api (push) Successful in 1m10s
API and ETL Build / build_etl (push) Successful in 1m45s
2025-09-11 20:53:28 -03:00
ddd99ec703 #34 add size based cache 2025-09-11 20:44:57 -03:00
965b693a19 tech stats
All checks were successful
API and ETL Build / build_api (push) Successful in 37s
API and ETL Build / build_etl (push) Successful in 39s
2025-06-19 21:41:16 -03:00
ecbf2f07d6 rate limiting e cpf masking 2025-06-19 19:56:17 -03:00
68d91b8151 order by no configuration model
All checks were successful
API and ETL Build / build_etl (push) Successful in 21s
API and ETL Build / build_api (push) Successful in 21s
2025-06-18 22:19:41 -03:00
afd6f0298c mais otimizações 3.0 2025-06-18 22:17:06 -03:00
fd9e4324dd otimizando o random 2025-06-18 21:49:09 -03:00
f16e1e5e5d melhorando otimizações de dados
All checks were successful
API and ETL Build / build_etl (push) Successful in 41s
API and ETL Build / build_api (push) Successful in 46s
2025-06-18 21:27:27 -03:00
4c72a68481 missed c.idcandidato
All checks were successful
API and ETL Build / build_etl (push) Successful in 15s
API and ETL Build / build_api (push) Successful in 26s
2025-06-18 20:07:19 -03:00
0d30afd700 pequenas otimizações
All checks were successful
API and ETL Build / build_etl (push) Successful in 4s
API and ETL Build / build_api (push) Successful in 15s
2025-06-18 20:01:52 -03:00
f5dda37285 adding otimizações
All checks were successful
API and ETL Build / build_etl (push) Successful in 1m48s
API and ETL Build / build_api (push) Successful in 1m51s
2025-06-18 19:17:05 -03:00
87a98fefb1 adding caching to get value sum
All checks were successful
API and ETL Build / build_etl (push) Successful in 3s
API and ETL Build / build_api (push) Successful in 10s
2025-06-13 11:47:39 -03:00
23256245a0 add estatistica
All checks were successful
API and ETL Build / build_etl (push) Successful in 52s
API and ETL Build / build_api (push) Successful in 11s
2025-06-12 21:00:34 -03:00
226d819909 add random candidato
All checks were successful
API and ETL Build / build_etl (push) Successful in 3s
API and ETL Build / build_api (push) Successful in 10s
2025-06-10 20:40:03 -03:00
23b1f0f14e altas mudanças
All checks were successful
API and ETL Build / build_etl (push) Successful in 30s
API and ETL Build / build_api (push) Successful in 15s
2025-06-10 20:16:22 -03:00
684a2c0630 adding disponibilidade de dados
All checks were successful
API and ETL Build / build_etl (push) Successful in 9s
API and ETL Build / build_api (push) Successful in 9s
2025-06-10 13:26:58 -03:00
673cda6408 adding cache
All checks were successful
API and ETL Build / build_etl (push) Successful in 39s
API and ETL Build / build_api (push) Successful in 11s
2025-06-09 23:31:21 -03:00
39faab6483 add popularidade ordering
All checks were successful
API and ETL Build / build_etl (push) Successful in 5s
API and ETL Build / build_api (push) Successful in 20s
2025-06-09 20:52:22 -03:00
5068d348af add db.sql back 2025-06-09 20:47:17 -03:00
93e08a0378 melhoria pesquisa
All checks were successful
API and ETL Build / build_etl (push) Successful in 1m1s
API and ETL Build / build_api (push) Successful in 19s
2025-06-09 20:22:41 -03:00
322e6034bc dumb fix
All checks were successful
API and ETL Build / build_etl (push) Successful in 39s
API and ETL Build / build_api (push) Successful in 10s
2025-06-07 15:29:11 -03:00
e57b3162db updates
All checks were successful
API and ETL Build / build_etl (push) Successful in 45s
API and ETL Build / build_api (push) Successful in 10s
2025-06-07 15:18:21 -03:00
9a107ce9e8 adding despesas e receitas 2025-06-07 11:56:03 -03:00
50 changed files with 2258 additions and 225 deletions

View File

@@ -8,6 +8,7 @@ namespace OpenCand.API.Config
public const string DefaultPolicy = "DefaultPolicy";
public const string CandidatoSearchPolicy = "CandidatoSearchPolicy";
public const string CpfRevealPolicy = "CpfRevealPolicy";
public const string EstatisticaPolicy = "EstatisticaPolicy";
public static void ConfigureRateLimiting(this IServiceCollection services)
{
@@ -26,13 +27,12 @@ namespace OpenCand.API.Config
// Default policy: 200 requests per minute with burst of 100
options.AddFixedWindowLimiter(policyName: DefaultPolicy, options =>
{
options.PermitLimit = 200;
options.PermitLimit = 400;
options.Window = TimeSpan.FromMinutes(1);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 100; // Burst capacity
});
// Candidato Search policy: 300 requests per minute with burst of 200
options.AddFixedWindowLimiter(policyName: CandidatoSearchPolicy, options =>
{
options.PermitLimit = 300;
@@ -41,23 +41,27 @@ namespace OpenCand.API.Config
options.QueueLimit = 200; // Burst capacity
});
// CPF Reveal policy: 15 requests per minute without burst
options.AddFixedWindowLimiter(policyName: CpfRevealPolicy, options =>
{
options.PermitLimit = 15;
options.PermitLimit = 20;
options.Window = TimeSpan.FromMinutes(1);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 0; // No burst
options.QueueLimit = 5; // Burst capacity
});
options.OnRejected = async (context, token) =>
{
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
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);
if (retryAfter.HasValue)
{
context.HttpContext.Response.Headers.Add("Retry-After", retryAfter.Value.ToString());
context.HttpContext.Response.Headers.Append("Retry-After", retryAfter.Value.ToString());
}
await context.HttpContext.Response.WriteAsync(

View File

@@ -1,10 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using OpenCand.API.Config;
namespace OpenCand.API.Controllers
{
[ApiController]
[Route("v1/[controller]")]
[Produces("application/json")]
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
public class BaseController : Controller
{

View File

@@ -1,14 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.VisualBasic;
using OpenCand.API.Config;
using OpenCand.API.Model;
using OpenCand.API.Services;
using OpenCand.Core.Models;
using OpenCand.Core.Utils;
namespace OpenCand.API.Controllers
{
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
public class CandidatoController : BaseController
{
private readonly OpenCandService openCandService;
@@ -22,13 +21,34 @@ namespace OpenCand.API.Controllers
[EnableRateLimiting(RateLimitingConfig.CandidatoSearchPolicy)]
public async Task<CandidatoSearchResult> CandidatoSearch([FromQuery] string q)
{
return await openCandService.SearchCandidatosAsync(q);
if (string.IsNullOrEmpty(q) || q.Length < 3)
{
throw new ArgumentException("Query parameter 'q' cannot be null/empty.", nameof(q));
}
var result = await openCandService.SearchCandidatosAsync(q);
result.Candidatos.ForEach(c => c.Cpf = CpfMasking.MaskCpf(c.Cpf));
return result;
}
[HttpGet("random")]
public async Task<object> GetRandomCandidatoId()
{
return new
{
idCandidato = await openCandService.GetRandomCandidato()
};
}
[HttpGet("{id}")]
public async Task<Candidato> GetCandidatoById([FromRoute] Guid id)
{
return await openCandService.GetCandidatoAsync(id);
var result = await openCandService.GetCandidatoAsync(id);
result.Cpf = CpfMasking.MaskCpf(result.Cpf);
return result;
}
[HttpGet("{id}/bens")]
@@ -52,5 +72,17 @@ namespace OpenCand.API.Controllers
await Task.Delay(randomWait);
return await openCandService.GetCandidatoCpfById(id);
}
[HttpGet("{id}/despesas")]
public async Task<DespesasResult> GetCandidatoDespesas([FromRoute] Guid id)
{
return await openCandService.GetDespesasByIdAndYear(id);
}
[HttpGet("{id}/receitas")]
public async Task<ReceitaResult> GetCandidatoReceitas([FromRoute] Guid id)
{
return await openCandService.GetReceitasByIdAndYear(id);
}
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Mvc;
using OpenCand.API.Model;
using OpenCand.API.Services;
using static OpenCand.API.Model.GetValueSumRequest;
namespace OpenCand.API.Controllers
{
public class EstatisticaController : BaseController
{
private readonly EstatisticaService estatisticaService;
public EstatisticaController(EstatisticaService estatisticaService)
{
this.estatisticaService = estatisticaService;
}
[HttpGet("configuration")]
public async Task<ConfigurationModel> GetConfiguration()
{
return await estatisticaService.GetConfigurationModel();
}
[HttpGet("enriquecimento")]
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos([FromQuery] GetValueSumRequestFilter requestFilter)
{
return await estatisticaService.GetMaioresEnriquecimentos(requestFilter);
}
[HttpPost("values-sum")]
public async Task<List<GetValueSumResponse>> GetValuesSum([FromBody] GetValueSumRequest getValueSumRequest)
{
return await estatisticaService.GetValueSum(getValueSumRequest);
}
}
}

View File

@@ -21,5 +21,18 @@ namespace OpenCand.API.Controllers
{
return await openCandService.GetOpenCandStatsAsync();
}
[HttpGet("data-availability")]
public async Task<DataAvailabilityStats> GetDataAvailabilityStats()
{
return await openCandService.GetDataAvailabilityStatsAsync();
}
[HttpGet("tech")]
public async Task<DatabaseTechStats> GetDatabaseTechStats()
{
return await openCandService.GetDatabaseTechStatsAsync();
}
}
}

View File

@@ -0,0 +1,10 @@
namespace OpenCand.API.Model
{
public class ConfigurationModel
{
public List<string> Partidos { get; set; }
public List<string> SiglasUF { get; set; }
public List<int> Anos { get; set; }
public List<string> Cargos { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
namespace OpenCand.API.Model
{
public class MaioresEnriquecimento
{
public Guid IdCandidato { get; set; }
public string Nome { get; set; }
public float PatrimonioInicial { get; set; }
public int AnoInicial { get; set; }
public float PatrimonioFinal { get; set; }
public int AnoFinal { get; set; }
public float Enriquecimento { get; set; }
}
public class GetValueSumRequest
{
public string Type { get; set; } // "bem", "despesa", "receita"
public string GroupBy { get; set; } // "candidato", "partido", "uf", or "cargo"
public GetValueSumRequestFilter? Filter { get; set; }
public class GetValueSumRequestFilter
{
public string? Partido { get; set; } // Optional, can be null
public string? Uf { get; set; } // Optional, can be null
public int? Ano { get; set; } // Optional, can be null
public string? Cargo { get; set; } // Optional, can be null
}
}
public class GetValueSumResponse
{
public Guid? IdCandidato { get; set; }
public string? Sgpartido { get; set; }
public string? SiglaUf { get; set; }
public string? Cargo { get; set; }
public string? Nome { get; set; }
public int Ano { get; set; }
public decimal Valor { get; set; }
}
}

View File

@@ -21,4 +21,14 @@ namespace OpenCand.API.Model
{
public string Cpf { get; set; }
}
public class DespesasResult
{
public List<Despesa> Despesas { get; set; }
}
public class ReceitaResult
{
public List<Receita> Receitas { get; set; }
}
}

View File

@@ -1,3 +1,4 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using OpenCand.API.Config;
using OpenCand.API.Repository;
@@ -35,7 +36,8 @@ namespace OpenCand.API
{
FileProvider = new PhysicalFileProvider(Path.Combine(workingDir, "fotos_cand")),
RequestPath = "/assets/fotos"
}); app.UseHttpsRedirection();
});
app.UseHttpsRedirection();
// Use rate limiting middleware
app.UseRateLimiter();
@@ -56,10 +58,25 @@ namespace OpenCand.API
private static void SetupServices(WebApplicationBuilder builder)
{
builder.Services.Configure<FotosSettings>(builder.Configuration.GetSection("FotosSettings"));
builder.Services.AddScoped<OpenCandRepository>();
builder.Services.AddScoped<CandidatoRepository>();
builder.Services.AddScoped<BemCandidatoRepository>();
// Configure memory cache with size limit from appsettings
var sizeLimitMB = builder.Configuration.GetValue<int>("CacheSettings:SizeLimitMB", 15);
builder.Services.AddMemoryCache(options =>
{
options.SizeLimit = sizeLimitMB * 1024L * 1024L; // Convert MB to bytes
});
builder.Services.AddScoped(provider => new OpenCandRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
builder.Services.AddScoped(provider => new CandidatoRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
builder.Services.AddScoped(provider => new BemCandidatoRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
builder.Services.AddScoped(provider => new DespesaReceitaRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
builder.Services.AddScoped(provider => new EstatisticaRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
builder.Services.AddScoped<OpenCandService>();
builder.Services.AddScoped<EstatisticaService>();
// Add cache preload background service
builder.Services.AddHostedService<CachePreloadService>();
}
}
}

View File

@@ -1,5 +1,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Caching.Memory;
using Npgsql;
using System.Text.Json;
using System.Text;
namespace OpenCand.Repository
{
@@ -7,11 +9,153 @@ namespace OpenCand.Repository
{
protected string ConnectionString { get; private set; }
protected NpgsqlConnection? Connection { get; private set; }
protected readonly IMemoryCache? _cache;
public BaseRepository(IConfiguration configuration)
// Default cache settings
protected static readonly TimeSpan DefaultCacheExpiration = TimeSpan.MaxValue;
protected static readonly CacheItemPriority DefaultCachePriority = CacheItemPriority.Normal;
public BaseRepository(IConfiguration configuration, IMemoryCache? cache = null)
{
ConnectionString = configuration["DatabaseSettings:ConnectionString"] ??
throw new ArgumentNullException("Connection string not found in configuration");
_cache = cache;
}
/// <summary>
/// Generic method to get data from cache or execute a factory function if not cached
/// </summary>
/// <typeparam name="T">Type of data to cache</typeparam>
/// <param name="cacheKey">Unique cache key</param>
/// <param name="factory">Function to execute if data is not in cache</param>
/// <param name="expiration">Cache expiration time (optional, uses default if not provided)</param>
/// <param name="priority">Cache priority (optional, uses default if not provided)</param>
/// <returns>Cached or freshly retrieved data</returns>
protected async Task<T?> GetOrSetCacheAsync<T>(
string cacheKey,
Func<Task<T?>> factory,
TimeSpan? expiration = null,
CacheItemPriority? priority = null) where T : class
{
// If caching is not available, execute factory directly
if (_cache == null)
{
return await factory();
}
// Try to get cached data first
if (_cache.TryGetValue(cacheKey, out T? cachedData) && cachedData != null)
{
return cachedData;
}
// If not in cache, execute factory function
var result = await factory();
// Only cache non-null results
if (result != null)
{
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
{
SlidingExpiration = expiration ?? DefaultCacheExpiration,
Priority = priority ?? DefaultCachePriority,
Size = EstimateSize(result)
});
}
return result;
}
/// <summary>
/// Generic method to get data from cache or execute a synchronous factory function if not cached
/// </summary>
/// <typeparam name="T">Type of data to cache</typeparam>
/// <param name="cacheKey">Unique cache key</param>
/// <param name="factory">Function to execute if data is not in cache</param>
/// <param name="expiration">Cache expiration time (optional, uses default if not provided)</param>
/// <param name="priority">Cache priority (optional, uses default if not provided)</param>
/// <returns>Cached or freshly retrieved data</returns>
protected T? GetOrSetCache<T>(
string cacheKey,
Func<T?> factory,
TimeSpan? expiration = null,
CacheItemPriority? priority = null) where T : class
{
// If caching is not available, execute factory directly
if (_cache == null)
{
return factory();
}
// Try to get cached data first
if (_cache.TryGetValue(cacheKey, out T? cachedData) && cachedData != null)
{
return cachedData;
}
// If not in cache, execute factory function
var result = factory();
// Only cache non-null results
if (result != null)
{
_cache.Set(cacheKey, result, new MemoryCacheEntryOptions
{
SlidingExpiration = expiration ?? DefaultCacheExpiration,
Priority = priority ?? DefaultCachePriority,
Size = EstimateSize(result)
});
}
return result;
}
/// <summary>
/// Removes an item from cache by key
/// </summary>
/// <param name="cacheKey">Cache key to remove</param>
protected void ClearCache(string cacheKey)
{
_cache?.Remove(cacheKey);
}
/// <summary>
/// Checks if an item exists in cache
/// </summary>
/// <param name="cacheKey">Cache key to check</param>
/// <returns>True if item exists in cache, false otherwise</returns>
protected bool IsCached(string cacheKey)
{
return _cache?.TryGetValue(cacheKey, out _) ?? false;
} /// <summary>
/// Generates a standardized cache key for entity-based caching
/// </summary>
/// <param name="entityName">Name of the entity (e.g., "Candidato", "Stats")</param>
/// <param name="identifier">Unique identifier for the entity (optional)</param>
/// <returns>Formatted cache key</returns>
protected static string GenerateCacheKey(string entityName, object? identifier = null)
{
return identifier != null ? $"{entityName}_{identifier}" : entityName;
}
/// <summary>
/// Estimates the memory size of an object by serializing it to JSON
/// </summary>
/// <typeparam name="T">Type of the object</typeparam>
/// <param name="obj">The object to estimate size for</param>
/// <returns>Estimated size in bytes</returns>
private static long EstimateSize<T>(T obj)
{
if (obj == null) return 0;
try
{
var json = JsonSerializer.Serialize(obj);
return Encoding.UTF8.GetByteCount(json);
}
catch
{
return 1024; // Default estimate if serialization fails
}
}
}
}

View File

@@ -1,4 +1,5 @@
using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Npgsql;
using OpenCand.Core.Models;
@@ -6,7 +7,7 @@ namespace OpenCand.Repository
{
public class BemCandidatoRepository : BaseRepository
{
public BemCandidatoRepository(IConfiguration configuration) : base(configuration)
public BemCandidatoRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
{
}

View File

@@ -1,4 +1,5 @@
using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Npgsql;
using OpenCand.Core.Models;
@@ -6,40 +7,35 @@ namespace OpenCand.Repository
{
public class CandidatoRepository : BaseRepository
{
public CandidatoRepository(IConfiguration configuration) : base(configuration)
public CandidatoRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
{
}
public async Task<List<Candidato>> SearchCandidatosAsync(string query)
public async Task<List<Candidato>?> SearchCandidatosAsync(string query)
{
using (var connection = new NpgsqlConnection(ConnectionString))
string cacheKey = GenerateCacheKey("Search", query);
return await GetOrSetCacheAsync(cacheKey, async () =>
{
using var connection = new NpgsqlConnection(ConnectionString);
return (await connection.QueryAsync<Candidato>(@"
SELECT *,
CASE
WHEN lower(apelido) = lower(@query) THEN 0 -- apelido Exact match (case-insensitive)
WHEN lower(apelido) LIKE lower(@query) || '%' THEN 1 -- apelido Starts with match (case-insensitive)
WHEN lower(apelido) LIKE '%' || lower(@query) THEN 2 -- apelido Contains anywhere match (case-insensitive)
WHEN lower(nome) = lower(@query) THEN 0 -- nome Exact match (case-insensitive)
WHEN lower(nome) LIKE lower(@query) || '%' THEN 1 -- nome Starts with match (case-insensitive)
WHEN lower(nome) LIKE '%' || lower(@query) THEN 2 -- nome Contains anywhere match (case-insensitive)
WHEN cpf = @query THEN 0 -- cpf Exact match for CPF
WHEN cpf LIKE @query || '%' THEN 1 -- cpf Starts with match for CPF
WHEN cpf LIKE '%' || @query THEN 2 -- cpf Contains anywhere match for CPF
ELSE 3
END AS name_rank
FROM candidato
WHERE apelido ILIKE '%' || @query || '%' OR
nome ILIKE '%' || @query || '%' OR
cpf ILIKE '%' || @query || '%'
ORDER BY name_rank,
length(nome) ASC
LIMIT 10;",
new { query })).AsList();
}
SELECT c.*,
GREATEST(similarity(c.apelido, @q), similarity(c.nome, @q)) AS sim
FROM candidato c
WHERE c.apelido % @q
OR c.nome % @q
ORDER BY c.popularidade DESC, sim DESC, length(c.nome) ASC
LIMIT 10;
", new { q = query })).AsList();
});
}
public async Task<Candidato?> GetCandidatoAsync(Guid idcandidato)
{
string cacheKey = GenerateCacheKey("Candidato", idcandidato);
return await GetOrSetCacheAsync(cacheKey, async () =>
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
@@ -48,6 +44,7 @@ namespace OpenCand.Repository
WHERE idcandidato = @idcandidato;",
new { idcandidato });
}
});
}
public async Task<string?> GetCandidatoCpfAsync(Guid idcandidato)
@@ -93,5 +90,37 @@ namespace OpenCand.Repository
return (await connection.QueryAsync<RedeSocial>(query, new { idcandidato })).AsList();
}
}
public async Task<List<CandidatoExt>?> GetCandidatoExtById(Guid idcandidato)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
var query = @"
SELECT * FROM candidato_ext
WHERE idcandidato = @idcandidato";
return (await connection.QueryAsync<CandidatoExt>(query, new { idcandidato })).AsList();
}
}
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<Guid?> GetRandomCandidatoIdAsync()
{
using var connection = new NpgsqlConnection(ConnectionString);
return await connection.QueryFirstOrDefaultAsync<Guid?>(@"
SELECT idcandidato
FROM candidato
TABLESAMPLE SYSTEM (0.01)
LIMIT 1;
");
}
}
}

View File

@@ -0,0 +1,43 @@
using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Npgsql;
using OpenCand.Core.Models;
namespace OpenCand.Repository
{
public class DespesaReceitaRepository : BaseRepository
{
public DespesaReceitaRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
{
}
public async Task<List<Despesa>> GetDespesasByCandidatoIdYearAsync(Guid idcandidato)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
return (await connection.QueryAsync<Despesa>(@"
SELECT * FROM despesas_candidato
WHERE idcandidato = @idcandidato
ORDER BY valor DESC;", new
{
idcandidato
})).AsList();
}
}
public async Task<List<Receita>> GetReceitasByCandidatoIdYearAsync(Guid idcandidato)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
return (await connection.QueryAsync<Receita>(@"
SELECT * FROM receitas_candidato
WHERE idcandidato = @idcandidato
ORDER BY valor DESC;", new
{
idcandidato
})).AsList();
}
}
}
}

View File

@@ -0,0 +1,124 @@
using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Npgsql;
using OpenCand.API.Model;
using OpenCand.Repository;
using System.Text.Json;
using static OpenCand.API.Model.GetValueSumRequest;
namespace OpenCand.API.Repository
{
public class EstatisticaRepository : BaseRepository
{
private readonly IConfiguration configuration;
public EstatisticaRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
{
this.configuration = configuration;
}
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos(GetValueSumRequestFilter? requestFilter = null)
{
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<string>();
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 () =>
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
return (await connection.QueryAsync<MaioresEnriquecimento>(@"
WITH extremos_declaracao AS (
SELECT
idcandidato,
MIN(ano) AS anoInicial,
MAX(ano) AS anoFinal
FROM mv_bem_candidato
GROUP BY idcandidato
HAVING COUNT(ano) >= 2
)
SELECT
ed.idcandidato,
c.nome,
ed.anoInicial,
pi.valor AS patrimonioInicial,
ed.anoFinal,
pf.valor AS patrimonioFinal,
(pf.valor - pi.valor) AS enriquecimento
FROM extremos_declaracao ed
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;")
).AsList();
}
});
return result ?? new List<MaioresEnriquecimento>();
}
public async Task<ConfigurationModel> GetConfiguration()
{
string cacheKey = GenerateCacheKey("GetConfigurationModel");
var result = await GetOrSetCacheAsync(cacheKey, async () =>
{
var result = new ConfigurationModel();
using (var connection = new NpgsqlConnection(ConnectionString))
{
result.Partidos = (await connection.QueryAsync<string>(@"SELECT DISTINCT sigla FROM partido ORDER BY sigla ASC;")).AsList();
result.SiglasUF = (await connection.QueryAsync<string>(@"SELECT DISTINCT siglauf FROM candidato_mapping ORDER BY siglauf ASC;")).AsList();
result.Anos = (await connection.QueryAsync<int>(@"SELECT DISTINCT ano FROM candidato_mapping ORDER BY ano DESC;")).AsList();
result.Cargos = (await connection.QueryAsync<string>(@"SELECT DISTINCT cargo FROM candidato_mapping ORDER BY cargo ASC;")).AsList();
}
return result;
});
return result ?? new ConfigurationModel();
}
public async Task<List<GetValueSumResponse>> GetValueSum(string query, Dictionary<string, object>? parameters = null)
{
string cacheKey = GenerateCacheKey(query.GetHashCode().ToString());
return await GetOrSetCacheAsync(cacheKey, async () =>
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
return (await connection.QueryAsync<GetValueSumResponse>(query, parameters)).AsList();
}
}) ?? new List<GetValueSumResponse>();
}
}
}

View File

@@ -1,4 +1,5 @@
using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Npgsql;
using OpenCand.Core.Models;
using OpenCand.Repository;
@@ -7,11 +8,18 @@ namespace OpenCand.API.Repository
{
public class OpenCandRepository : BaseRepository
{
public OpenCandRepository(IConfiguration configuration) : base(configuration)
private readonly IConfiguration configuration;
public OpenCandRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
{
this.configuration = configuration;
}
public async Task<OpenCandStats> GetOpenCandStatsAsync()
{
string cacheKey = GenerateCacheKey("OpenCandStats");
var result = await GetOrSetCacheAsync(cacheKey, async () =>
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
@@ -22,8 +30,114 @@ namespace OpenCand.API.Repository
(SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos,
(SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais,
(SELECT COUNT(DISTINCT ano) FROM candidato_mapping) AS TotalEleicoes;");
return stats ?? new OpenCandStats();
}
});
return result ?? new OpenCandStats();
}
public async Task<DataAvailabilityStats> GetDataAvailabilityAsync()
{
string cacheKey = GenerateCacheKey("DataAvailabilityStats");
var result = await GetOrSetCacheAsync(cacheKey, async () =>
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
var stats = new DataAvailabilityStats();
// Get years for each data type separately
var candidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM candidato_mapping ORDER BY ano DESC");
var bemCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM bem_candidato ORDER BY ano DESC");
var despesaCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM despesas_candidato ORDER BY ano DESC");
var receitaCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM receitas_candidato ORDER BY ano DESC");
var redeSocialCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM rede_social ORDER BY ano DESC");
stats.Candidatos = candidatosYears.ToList();
stats.BemCandidatos = bemCandidatosYears.ToList();
stats.DespesaCandidatos = despesaCandidatosYears.ToList();
stats.ReceitaCandidatos = receitaCandidatosYears.ToList();
stats.RedeSocialCandidatos = redeSocialCandidatosYears.ToList();
// Get all folders from appsetting `FotosSettings__BasePath`
string basePath = configuration["FotosSettings:Path"] ?? string.Empty;
if (string.IsNullOrEmpty(basePath))
throw new InvalidOperationException("Base path for photos is not configured.");
var directories = Directory.GetDirectories(basePath);
if (directories.Any())
stats.FotosCandidatos = directories
.Select(dir => dir.Split(Path.DirectorySeparatorChar).Last().Split("_")[1].Replace("cand", ""))
.Select(ano => Convert.ToInt32(ano))
.Distinct()
.OrderByDescending(ano => ano)
.ToList();
return stats;
}
});
return result ?? new DataAvailabilityStats();
}
public async Task<DatabaseTechStats> GetDatabaseTechStatsAsync()
{
string cacheKey = GenerateCacheKey("DatabaseTechStats");
var result = await GetOrSetCacheAsync(cacheKey, async () =>
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
var stats = new DatabaseTechStats();
// Get table stats using reltuples for entries
var tableStats = await connection.QueryAsync<TableStats>(@"
SELECT
pt.schemaname||'.'||pt.tablename as Name,
pg_total_relation_size(pt.schemaname||'.'||pt.tablename) as TotalSize,
COALESCE(c.reltuples,0)::bigint as Entries
FROM pg_tables pt
JOIN pg_class c ON c.relname = pt.tablename AND c.relkind = 'r'
WHERE pt.schemaname = 'public'
ORDER BY pg_total_relation_size(pt.schemaname||'.'||pt.tablename) DESC;");
var tableStatsList = tableStats.ToList();
stats.Tables = tableStatsList;
stats.TotalSize = tableStatsList.Sum(t => t.TotalSize);
stats.TotalEntries = tableStatsList.Sum(t => t.Entries);
// Get materialized view stats using reltuples for entries
var materializedViewStats = await connection.QueryAsync<TableStats>(@"
SELECT
pmv.schemaname||'.'||pmv.matviewname as Name,
pg_total_relation_size(pmv.schemaname||'.'||pmv.matviewname) as TotalSize,
COALESCE(c.reltuples,0)::bigint as Entries
FROM pg_matviews pmv
JOIN pg_class c ON c.relname = pmv.matviewname AND c.relkind = 'm'
WHERE pmv.schemaname = 'public'
ORDER BY pg_total_relation_size(pmv.schemaname||'.'||pmv.matviewname) DESC;");
stats.MaterializedViews = materializedViewStats.ToList();
// Get index stats
var indexStats = await connection.QueryFirstOrDefaultAsync<IndexStats>(@"
SELECT
COUNT(*) as Amount,
SUM(pg_relation_size(indexrelid)) as Size
FROM pg_stat_user_indexes
WHERE schemaname = 'public';");
stats.Indexes = indexStats ?? new IndexStats();
return stats;
}
});
return result ?? new DatabaseTechStats();
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using Microsoft.Extensions.Logging;
using OpenCand.API.Model;
namespace OpenCand.API.Services
{
public class CachePreloadService : BackgroundService
{
private readonly IServiceProvider serviceProvider;
private readonly ILogger<CachePreloadService> logger;
public CachePreloadService(IServiceProvider serviceProvider, ILogger<CachePreloadService> logger)
{
this.serviceProvider = serviceProvider;
this.logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Wait a bit for the application to fully start up
await Task.Delay(5000, stoppingToken);
logger.LogInformation("Starting cache preload process...");
using var scope = serviceProvider.CreateScope();
var estatisticaService = scope.ServiceProvider.GetRequiredService<EstatisticaService>();
var openCandService = scope.ServiceProvider.GetRequiredService<OpenCandService>();
// First, preload single-call endpoints
await PreloadSingleEndpoints(estatisticaService, openCandService);
await PerformPreLoadEstatistica(estatisticaService);
logger.LogInformation("Cache preload process completed.");
}
private async Task PerformPreLoadEstatistica(EstatisticaService estatisticaService)
{
await PerformPreLoad("GetConfigurationModel", estatisticaService.GetConfigurationModel);
var types = new[] { "bem", "despesa", "receita" };
var groupByValues = new[] { "candidato", "partido", "uf", "cargo" };
logger.LogInformation($"Preloading cache with GetValueSum requests (3 types * 4 groupBy combinations)");
foreach (var type in types)
{
foreach (var groupBy in groupByValues)
{
var request = new GetValueSumRequest
{
Type = type,
GroupBy = groupBy,
Filter = null // No filters as requested
};
logger.LogDebug($"Executing cache preload request: Type={type}, GroupBy={groupBy}");
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", openCandService.GetOpenCandStatsAsync);
await PerformPreLoad("GetDatabaseTechStatsAsync", openCandService.GetDatabaseTechStatsAsync);
await PerformPreLoad("GetDataAvailabilityStatsAsync", openCandService.GetDataAvailabilityStatsAsync);
logger.LogInformation("Single-call endpoints preload completed");
}
private async Task PerformPreLoad(string name, Func<Task> action)
{
try
{
logger.LogDebug($"Executing cache preload for {name}");
var startTime = DateTime.UtcNow;
await action();
var duration = DateTime.UtcNow - startTime;
logger.LogInformation($"Cache preload completed for {name}: Duration={duration.TotalMilliseconds}ms");
}
catch (Exception ex)
{
logger.LogError(ex, $"Failed to perform preload action for {name}");
}
finally
{
await Task.Delay(100); // Small delay to avoid overwhelming the database
}
}
}
}

View File

@@ -0,0 +1,176 @@
using OpenCand.API.Model;
using OpenCand.API.Repository;
using static OpenCand.API.Model.GetValueSumRequest;
namespace OpenCand.API.Services
{
public class EstatisticaService
{
private readonly EstatisticaRepository estatisticaRepository;
private readonly ILogger<OpenCandService> logger;
public EstatisticaService(
EstatisticaRepository estatisticaRepository,
ILogger<OpenCandService> logger)
{
this.estatisticaRepository = estatisticaRepository;
this.logger = logger;
}
public async Task<ConfigurationModel> GetConfigurationModel()
{
logger.LogInformation("Getting configuration model");
return await estatisticaRepository.GetConfiguration();
}
public async Task<List<MaioresEnriquecimento>> GetMaioresEnriquecimentos(GetValueSumRequestFilter? requestFilter = null)
{
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<List<GetValueSumResponse>> 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();
var result = await estatisticaRepository.GetValueSum(query, parameters);
stopwatch.Stop();
logger.LogInformation($"GetValueSum - Execution time: {stopwatch.ElapsedMilliseconds} ms");
return result;
}
private void ValidateRequest(GetValueSumRequest request)
{
if (string.IsNullOrWhiteSpace(request.Type))
throw new ArgumentException("Type is required.");
if (string.IsNullOrWhiteSpace(request.GroupBy))
throw new ArgumentException("GroupBy is required.");
var validTypes = new[] { "bem", "despesa", "receita" };
if (!validTypes.Contains(request.Type.ToLower()))
throw new ArgumentException($"Invalid type specified. Valid values are: {string.Join(", ", validTypes)}");
var validGroupBy = new[] { "candidato", "partido", "uf", "cargo" };
if (!validGroupBy.Contains(request.GroupBy.ToLower()))
throw new ArgumentException($"Invalid groupBy specified. Valid values are: {string.Join(", ", validGroupBy)}");
}
private class SqlQueryBuilder
{
private readonly Dictionary<string, object> _parameters = new();
private int _paramCounter = 0;
public Dictionary<string, object> GetParameters() => _parameters;
public string BuildQuery(GetValueSumRequest request)
{
var selectClause = BuildSelectClause(request.GroupBy);
var fromClause = BuildFromClause(request.Type);
var joinClause = BuildJoinClause(request.GroupBy);
var whereClause = BuildWhereClause(request.Filter);
var groupByClause = BuildGroupByClause(request.GroupBy);
var orderByClause = "ORDER BY SUM(src.valor) DESC";
var limitClause = "LIMIT 10";
return $"{selectClause} {fromClause} {joinClause} {whereClause} {groupByClause} {orderByClause} {limitClause}";
}
private string BuildSelectClause(string groupBy)
{
return groupBy.ToLower() switch
{
"candidato" => "SELECT src.idcandidato, cm.nome, src.ano, SUM(src.valor) as valor",
"partido" => "SELECT cm.sgpartido, src.ano, SUM(src.valor) as valor",
"uf" => "SELECT cm.siglauf, src.ano, SUM(src.valor) as valor",
"cargo" => "SELECT cm.cargo, src.ano, SUM(src.valor) as valor",
_ => throw new ArgumentException("Invalid group by specified.")
};
}
private string BuildFromClause(string type)
{
return type.ToLower() switch
{
"bem" => "FROM mv_bem_candidato src",
"despesa" => "FROM mv_despesas_candidato src",
"receita" => "FROM mv_receitas_candidato src",
_ => throw new ArgumentException("Invalid type specified.")
};
}
private string BuildJoinClause(string groupBy)
{
return groupBy.ToLower() switch
{
"candidato" => "JOIN mv_candidato_mapping_analytics cm ON src.idcandidato = cm.idcandidato AND src.ano = cm.ano",
"partido" or "uf" or "cargo" => "JOIN mv_candidato_mapping_analytics cm ON src.idcandidato = cm.idcandidato AND src.ano = cm.ano",
_ => throw new ArgumentException("Invalid group by specified.")
};
}
private string BuildWhereClause(GetValueSumRequest.GetValueSumRequestFilter? filter)
{
if (filter == null)
return "";
var conditions = new List<string>();
if (!string.IsNullOrWhiteSpace(filter.Partido))
{
var paramName = $"partido{++_paramCounter}";
conditions.Add($"cm.sgpartido = @{paramName}");
_parameters[paramName] = filter.Partido;
}
if (!string.IsNullOrWhiteSpace(filter.Uf))
{
var paramName = $"uf{++_paramCounter}";
conditions.Add($"cm.siglauf = @{paramName}");
_parameters[paramName] = filter.Uf;
}
if (filter.Ano.HasValue)
{
var paramName = $"ano{++_paramCounter}";
conditions.Add($"cm.ano = @{paramName}");
_parameters[paramName] = filter.Ano.Value;
}
if (!string.IsNullOrWhiteSpace(filter.Cargo))
{
var paramName = $"cargo{++_paramCounter}";
conditions.Add($"cm.cargo = @{paramName}");
_parameters[paramName] = filter.Cargo;
}
return conditions.Count > 0 ? $"WHERE {string.Join(" AND ", conditions)}" : "";
}
private string BuildGroupByClause(string groupBy)
{
return groupBy.ToLower() switch
{
"candidato" => "GROUP BY src.idcandidato, cm.nome, src.ano",
"partido" => "GROUP BY cm.sgpartido, src.ano",
"uf" => "GROUP BY cm.siglauf, src.ano",
"cargo" => "GROUP BY cm.cargo, src.ano",
_ => throw new ArgumentException("Invalid group by specified.")
};
}
}
}
}

View File

@@ -12,6 +12,7 @@ namespace OpenCand.API.Services
private readonly OpenCandRepository openCandRepository;
private readonly CandidatoRepository candidatoRepository;
private readonly BemCandidatoRepository bemCandidatoRepository;
private readonly DespesaReceitaRepository despesaReceitaRepository;
private readonly IConfiguration configuration;
private readonly FotosSettings fotoSettings;
private readonly ILogger<OpenCandService> logger;
@@ -20,6 +21,7 @@ namespace OpenCand.API.Services
OpenCandRepository openCandRepository,
CandidatoRepository candidatoRepository,
BemCandidatoRepository bemCandidatoRepository,
DespesaReceitaRepository despesaReceitaRepository,
IOptions<FotosSettings> fotoSettings,
IConfiguration configuration,
ILogger<OpenCandService> logger)
@@ -27,6 +29,7 @@ namespace OpenCand.API.Services
this.openCandRepository = openCandRepository;
this.candidatoRepository = candidatoRepository;
this.bemCandidatoRepository = bemCandidatoRepository;
this.despesaReceitaRepository = despesaReceitaRepository;
this.fotoSettings = fotoSettings.Value;
this.configuration = configuration;
this.logger = logger;
@@ -34,25 +37,62 @@ namespace OpenCand.API.Services
public async Task<OpenCandStats> GetOpenCandStatsAsync()
{
logger.LogInformation("Getting OpenCand stats");
return await openCandRepository.GetOpenCandStatsAsync();
}
public async Task<DataAvailabilityStats> GetDataAvailabilityStatsAsync()
{
logger.LogInformation("Getting data availability stats");
var stats = await openCandRepository.GetDataAvailabilityAsync();
return stats;
}
public async Task<DatabaseTechStats> GetDatabaseTechStatsAsync()
{
logger.LogInformation("Getting database tech stats");
var stats = await openCandRepository.GetDatabaseTechStatsAsync();
stats.Tables = stats.Tables.OrderBy(t => t.Name).ToList();
stats.MaterializedViews = stats.MaterializedViews.OrderBy(mv => mv.Name).ToList();
return stats;
}
public async Task<CandidatoSearchResult> 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<Candidato>()
};
}
public async Task<Guid?> GetRandomCandidato()
{
logger.LogInformation("Getting random candidato");
return await candidatoRepository.GetRandomCandidatoIdAsync();
}
public async Task<Candidato> GetCandidatoAsync(Guid idcandidato)
{
var result = await candidatoRepository.GetCandidatoAsync(idcandidato);
var eleicoes = await candidatoRepository.GetCandidatoMappingById(idcandidato);
var candidatoExt = await candidatoRepository.GetCandidatoExtById(idcandidato);
foreach (var eleicao in eleicoes)
try
{
eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido);
await candidatoRepository.IncreaseCandidatoPopularity(idcandidato);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error increasing popularity for Candidato ID {idcandidato}");
}
if (result == null)
@@ -63,11 +103,23 @@ 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)
{
eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido);
}
var lastEleicao = eleicoes.OrderByDescending(e => e.Ano).First();
result.FotoUrl = $"{fotoSettings.ApiBasePath}/foto_cand{lastEleicao.Ano}_{lastEleicao.SiglaUF}_div/F{lastEleicao.SiglaUF}{lastEleicao.SqCandidato}_div.jpg";
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;
}
@@ -80,6 +132,8 @@ namespace OpenCand.API.Services
result = new List<BemCandidato>();
}
logger.LogInformation($"Found {result.Count} bens for Candidato ID {idcandidato}");
return new BemCandidatoResult()
{
Bens = result.OrderByDescending(r => r.TipoBem).ThenByDescending(r => r.Valor).ToList()
@@ -88,12 +142,16 @@ namespace OpenCand.API.Services
public async Task<RedeSocialResult> GetCandidatoRedeSocialById(Guid idcandidato)
{
logger.LogInformation($"Getting redes sociais for Candidato ID {idcandidato}");
var result = await candidatoRepository.GetCandidatoRedeSocialById(idcandidato);
if (result == null)
{
result = new List<RedeSocial>();
}
logger.LogDebug($"Found {result.Count} redes sociais for Candidato ID {idcandidato}");
return new RedeSocialResult()
{
RedesSociais = result.OrderByDescending(r => r.Ano).ToList()
@@ -102,16 +160,56 @@ namespace OpenCand.API.Services
public async Task<CpfRevealResult> 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
};
}
public async Task<DespesasResult> 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()
};
}
public async Task<ReceitaResult> 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()
};
}
}
}

View File

@@ -1,16 +1,19 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Default": "Debug",
"Microsoft.AspNetCore": "Information"
}
},
"DatabaseSettings": {
"ConnectionString": "Host=localhost;Database=opencand;Username=root;Password=root;Include Error Detail=true;CommandTimeout=300"
"ConnectionString": "Host=localhost;Database=opencand;Username=root;Password=root;Pooling=true;Minimum Pool Size=1;Maximum Pool Size=20;Connection Lifetime=300;Command Timeout=30;Application Name=OpenCand.API;Include Error Detail=true"
},
"FotosSettings": {
"Path": "./fotos_cand",
"ApiBasePath": "http://localhost:5299/assets/fotos"
},
"CacheSettings": {
"SizeLimitMB": 15
},
"AllowedHosts": "*"
}

View File

@@ -1,33 +0,0 @@
#!/bin/bash
cd ./fotos_cand
COUNT=0
shopt -s nocaseglob
# Loop through all folders
for dir in */; do
# Change into the directory
cd "$dir" || continue
# Loop over every “.jpeg” (or “.JPEG”):
for f in *.jpeg; do
# “${f%.[jJ][pP][eE][gG]}” strips off the .jpeg/.JPEG suffix
base="${f%.[jJ][pP][eE][gG]}"
newfile="${base}.jpg"
# If theres already a .jpg with the same “base,” decide what to do:
if [ -e "$newfile" ]; then
echo "Skipping $f$newfile (target exists)"
# you could `rm "$f"` or move it to a backup folder here if you prefer
else
mv -v "$f" "$newfile"
fi
done
# Change back to the parent directory
cd ..
done
shopt -u nocaseglob
# Print a message indicating completion
echo "Normalization complete. Processed $COUNT files."

View File

@@ -20,14 +20,14 @@ namespace OpenCand.Core.Models
public string Sexo { get; set; }
public string EstadoCivil { get; set; }
public string Localidade { get; set; }
public string Escolaridade { get; set; }
public string Ocupacao { get; set; }
public int Ultimoano { get; set; }
public List<CandidatoMapping> Eleicoes { get; set; }
public List<CandidatoExt> CandidatoExt { get; set; }
// API ONLY
public string FotoUrl { get; set; }
}
@@ -37,9 +37,9 @@ namespace OpenCand.Core.Models
public Guid IdCandidato { get; set; }
public string Cpf { get; set; }
public string Nome { get; set; }
public string Apelido { get; set; }
public string SqCandidato { get; set; }
public int Ano { get; set; }
public string Turno { get; set; }
public string TipoEleicao { get; set; }
public string SiglaUF { get; set; }
public string NomeUE { get; set; }
@@ -51,6 +51,17 @@ namespace OpenCand.Core.Models
public Partido? Partido { get; set; } // Nullable to allow for candidates without a party
}
public class CandidatoExt
{
public Guid IdCandidato { get; set; }
public int Ano { get; set; }
public string Apelido { get; set; }
public string Email { get; set; }
public string EstadoCivil { get; set; }
public string Escolaridade { get; set; }
public string Ocupacao { get; set; }
}
public class RedeSocial
{
public Guid IdCandidato { get; set; }

View File

@@ -0,0 +1,25 @@
namespace OpenCand.Core.Models
{
public class Despesa
{
public Guid IdDespesa { get; set; }
public Guid IdCandidato { get; set; }
public int Ano { get; set; }
public int Turno { get; set; }
public string SqCandidato { get; set; }
public string SgPartido { get; set; }
public string TipoFornecedor { get; set; }
public string CpfFornecedor { get; set; }
public string CnpjFornecedor { get; set; }
public string NomeFornecedor { get; set; }
public string NomeFornecedorRFB { get; set; }
public string MunicipioFornecedor { get; set; }
public string TipoDocumento { get; set; }
public DateTime? DataDespesa { get; set; }
public string OrigemDespesa { get; set; }
public string Descricao { get; set; }
public float Valor { get; set; }
}
}

View File

@@ -8,4 +8,37 @@
public long TotalRedesSociais { get; set; }
public long TotalEleicoes { get; set; }
}
public class DataAvailabilityStats
{
public List<int> Candidatos { get; set; } = new List<int>();
public List<int> BemCandidatos { get; set; } = new List<int>();
public List<int> DespesaCandidatos { get; set; } = new List<int>();
public List<int> ReceitaCandidatos { get; set; } = new List<int>();
public List<int> RedeSocialCandidatos { get; set; } = new List<int>();
public List<int> FotosCandidatos { get; set; } = new List<int>();
}
public class DatabaseTechStats
{
public List<TableStats> Tables { get; set; } = new List<TableStats>();
public List<TableStats> MaterializedViews { get; set; } = new List<TableStats>();
public IndexStats Indexes { get; set; } = new IndexStats();
public long TotalSize { get; set; }
public long TotalEntries { get; set; }
}
public class TableStats
{
public string Name { get; set; } = string.Empty;
public long TotalSize { get; set; }
public long Entries { get; set; }
}
public class IndexStats
{
public int Amount { get; set; }
public long Size { get; set; }
}
}

View File

@@ -0,0 +1,26 @@
namespace OpenCand.Core.Models
{
public class Receita
{
public Guid IdReceita { get; set; }
public Guid IdCandidato { get; set; }
public int Ano { get; set; }
public int Turno { get; set; }
public string SqCandidato { get; set; }
public string SgPartido { get; set; }
public string FonteReceita { get; set; }
public string OrigemReceita { get; set; }
public string NaturezaReceita { get; set; }
public string EspecieReceita { get; set; }
public string CpfDoador { get; set; }
public string CnpjDoador { get; set; }
public string NomeDoador { get; set; }
public string NomeDoadorRFB { get; set; }
public string MunicipioDoador { get; set; }
public DateTime? DataReceita { get; set; }
public string Descricao { get; set; }
public float Valor { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
namespace OpenCand.Core.Utils
{
public static class CpfMasking
{
/// <summary>
/// Masks a CPF number by replacing the middle 3 digits with '*'
/// </summary>
/// <param name="cpf">The CPF number to mask.</param>
/// <returns>The masked CPF number.</returns>
public static string MaskCpf(string cpf)
{
if (string.IsNullOrEmpty(cpf) || cpf.Length != 11)
{
return cpf;
}
// Mask the middle 3 digits
return $"{cpf.Substring(0, 3)}***{cpf.Substring(6)}";
}
}
}

View File

@@ -5,5 +5,7 @@ namespace OpenCand.Config
public string CandidatosFolder { get; set; } = string.Empty;
public string BensCandidatosFolder { get; set; } = string.Empty;
public string RedesSociaisFolder { get; set; } = string.Empty;
public string ReceitaCandidatoFolder { get; set; } = string.Empty;
public string DespesaCandidatoFolder { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,15 @@
namespace OpenCand.ETL.Extensions
{
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string? value)
{
return string.IsNullOrEmpty(value) ||
value == "#NE" ||
value == "#NULO" ||
value == "#NULO#" ||
value == "NÃO DIVULGÁVEL" ||
value == "-4";
}
}
}

View File

@@ -1,15 +0,0 @@
using CsvHelper.Configuration;
using OpenCand.Parser.Models;
using System.Globalization;
namespace OpenCand.Parser.CsvMappers
{
public class BemCandidatoMap : ClassMap<BemCandidatoCSV>
{
public BemCandidatoMap()
{
AutoMap(CultureInfo.InvariantCulture);
// Explicitly handle any special mappings if needed
}
}
}

View File

@@ -1,15 +0,0 @@
using CsvHelper.Configuration;
using OpenCand.Parser.Models;
using System.Globalization;
namespace OpenCand.Parser.CsvMappers
{
public class CandidatoMap : ClassMap<CandidatoCSV>
{
public CandidatoMap()
{
AutoMap(CultureInfo.InvariantCulture);
// Explicitly handle any special mappings if needed
}
}
}

View File

@@ -1,14 +0,0 @@
using System.Globalization;
using CsvHelper.Configuration;
using OpenCand.Parser.Models;
namespace OpenCand.ETL.Parser.CsvMappers
{
public class RedeSocialMap : ClassMap<BemCandidatoCSV>
{
public RedeSocialMap()
{
AutoMap(CultureInfo.InvariantCulture);
}
}
}

View File

@@ -73,8 +73,10 @@ namespace OpenCand.Parser.Services
{
if (columns.Length > headerCount)
{
logger.LogCritical($"FixCsvFile - Line {i + 1} has {columns.Length} columns, expected {headerCount}. Halting process.");
return string.Empty; // Critical error, cannot fix this line => needs manual intervention
// logger.LogCritical($"FixCsvFile - Line {i + 1} has {columns.Length} columns, expected {headerCount}. Halting process.");
// return string.Empty; // Critical error, cannot fix this line => needs manual intervention
// consider as normal line
break;
}
logger.LogWarning($"FixCsvFile - Line {i + 1} has {columns.Length} columns, expected {headerCount}. Attempting to fix [i = {lineJump}]...");

View File

@@ -11,6 +11,9 @@ namespace OpenCand.Parser.Models
[Name("TP_ABRANGENCIA")]
public string TipoAbrangencia { get; set; }
[Name("NR_TURNO")]
public string Turno { get; set; }
[Name("SG_UF")]
public string SiglaUF { get; set; }
@@ -33,10 +36,10 @@ namespace OpenCand.Parser.Models
public string Apelido { get; set; }
[Name("NR_CPF_CANDIDATO")]
public string CPFCandidato { get; set; }
public string? CPFCandidato { get; set; }
[Name("DS_EMAIL", "NM_EMAIL")]
public string Email { get; set; }
public string? Email { get; set; }
[Name("DT_NASCIMENTO")]
public string DataNascimento { get; set; }
@@ -54,7 +57,7 @@ namespace OpenCand.Parser.Models
public string GrauInstrucao { get; set; }
[Name("DS_SIT_TOT_TURNO")]
public string SituacaoTurno { get; set; }
public string? SituacaoTurno { get; set; }
[Name("NR_PARTIDO")]
public int NumeroPartido { get; set; }

View File

@@ -0,0 +1,97 @@
using CsvHelper.Configuration.Attributes;
namespace OpenCand.Parser.Models
{
public class DespesasCSV
{
[Name("AA_ELEICAO")]
public int AnoEleicao { get; set; }
[Name("ST_TURNO")]
public string Turno { get; set; }
[Name("DT_PRESTACAO_CONTAS")]
public string DataPrestacaoContas { get; set; }
[Name("SQ_PRESTADOR_CONTAS")]
public string SequencialPrestadorContas { get; set; }
[Name("SG_UF")]
public string SiglaUF { get; set; }
[Name("NM_UE")]
public string NomeUE { get; set; }
[Name("NR_CNPJ_PRESTADOR_CONTA")]
public string CnpjPrestadorConta { get; set; }
[Name("SQ_CANDIDATO")]
public string SequencialCandidato { get; set; }
[Name("NR_CPF_CANDIDATO")]
public string CpfCandidato { get; set; }
[Name("SG_PARTIDO")]
public string SiglaPartido { get; set; }
[Name("DS_TIPO_FORNECEDOR")]
public string TipoFornecedor { get; set; }
[Name("CD_CNAE_FORNECEDOR")]
public string CodigoCnaeFornecedor { get; set; }
[Name("DS_CNAE_FORNECEDOR")]
public string DescricaoCnaeFornecedor { get; set; }
[Name("NR_CPF_CNPJ_FORNECEDOR")]
public string CpfCnpjFornecedor { get; set; }
[Name("NM_FORNECEDOR")]
public string NomeFornecedor { get; set; }
[Name("NM_FORNECEDOR_RFB")]
public string NomeFornecedorRFB { get; set; }
[Name("SG_UF_FORNECEDOR")]
public string SiglaUFFornecedor { get; set; }
[Name("NM_MUNICIPIO_FORNECEDOR")]
public string NomeMunicipioFornecedor { get; set; }
[Name("SQ_CANDIDATO_FORNECEDOR")]
public string SequencialCandidatoFornecedor { get; set; }
[Name("NR_CANDIDATO_FORNECEDOR")]
public string NumeroCandidatoFornecedor { get; set; }
[Name("DS_CARGO_FORNECEDOR")]
public string CargoFornecedor { get; set; }
[Name("NR_PARTIDO_FORNECEDOR")]
public string NumeroPartidoFornecedor { get; set; }
[Name("SG_PARTIDO_FORNECEDOR")]
public string SiglaPartidoFornecedor { get; set; }
[Name("NM_PARTIDO_FORNECEDOR")]
public string NomePartidoFornecedor { get; set; }
[Name("DS_TIPO_DOCUMENTO")]
public string TipoDocumento { get; set; }
[Name("DS_ORIGEM_DESPESA")]
public string OrigemDespesa { get; set; }
[Name("SQ_DESPESA")]
public string SequencialDespesa { get; set; }
[Name("DT_DESPESA")]
public string DataDespesa { get; set; }
[Name("DS_DESPESA")]
public string DescricaoDespesa { get; set; }
[Name("VR_DESPESA_CONTRATADA")]
public float ValorDespesaContratada { get; set; }
}
}

View File

@@ -0,0 +1,94 @@
using CsvHelper.Configuration.Attributes;
namespace OpenCand.Parser.Models
{
public class ReceitasCSV
{
[Name("AA_ELEICAO")]
public int AnoEleicao { get; set; }
[Name("ST_TURNO")]
public string Turno { get; set; }
[Name("SQ_PRESTADOR_CONTAS")]
public string SequencialPrestadorContas { get; set; }
[Name("SG_UF")]
public string SiglaUF { get; set; }
[Name("NM_UE")]
public string NomeUE { get; set; }
[Name("NR_CNPJ_PRESTADOR_CONTA")]
public string CnpjPrestadorConta { get; set; }
[Name("SQ_CANDIDATO")]
public string SequencialCandidato { get; set; }
[Name("NR_CPF_CANDIDATO")]
public string CpfCandidato { get; set; }
[Name("SG_PARTIDO")]
public string SiglaPartido { get; set; }
[Name("DS_FONTE_RECEITA")]
public string FonteReceita { get; set; }
[Name("DS_ORIGEM_RECEITA")]
public string OrigemReceita { get; set; }
[Name("DS_NATUREZA_RECEITA")]
public string NaturezaReceita { get; set; }
[Name("DS_ESPECIE_RECEITA")]
public string EspecieReceita { get; set; }
[Name("CD_CNAE_DOADOR")]
public string CodigoCnaeDoador { get; set; }
[Name("DS_CNAE_DOADOR")]
public string DescricaoCnaeDoador { get; set; }
[Name("NR_CPF_CNPJ_DOADOR")]
public string CpfCnpjDoador { get; set; }
[Name("NM_DOADOR")]
public string NomeDoador { get; set; }
[Name("NM_DOADOR_RFB")]
public string NomeDoadorRFB { get; set; }
[Name("SG_UF_DOADOR")]
public string SiglaUFDoaror { get; set; }
[Name("NM_MUNICIPIO_DOADOR")]
public string NomeMunicipioDoador { get; set; }
[Name("SQ_CANDIDATO_DOADOR")]
public string SequencialCandidatoDoador { get; set; }
[Name("SG_PARTIDO_DOADOR")]
public string SiglaPartidoDoador { get; set; }
[Name("NR_RECIBO_DOACAO")]
public string NumeroReciboDoacao { get; set; }
[Name("NR_DOCUMENTO_DOACAO")]
public string NumeroDocumentoDoacao { get; set; }
[Name("SQ_RECEITA")]
public string SequencialReceita { get; set; }
[Name("DT_RECEITA")]
public string DataReceita { get; set; }
[Name("DS_RECEITA")]
public string DescricaoReceita { get; set; }
[Name("VR_RECEITA")]
public float ValorReceita { get; set; }
[Name("DS_GENERO")]
public string Genero { get; set; }
}
}

View File

@@ -2,6 +2,8 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenCand.Config;
using OpenCand.ETL.Repository;
using OpenCand.ETL.Services;
using OpenCand.Parser.Models;
using OpenCand.Parser.Services;
@@ -15,6 +17,12 @@ namespace OpenCand.Parser
private readonly CsvParserService<CandidatoCSV> candidatoParserService;
private readonly CsvParserService<BemCandidatoCSV> bemCandidatoParserService;
private readonly CsvParserService<RedeSocialCSV> redeSocialParserService;
private readonly CsvParserService<DespesasCSV> despesaParserService;
private readonly CsvParserService<ReceitasCSV> receitaParserService;
private readonly DespesaReceitaService despesaReceitaService;
private readonly ViewRepository viewRepository;
private readonly string BasePath;
@@ -24,7 +32,11 @@ namespace OpenCand.Parser
IConfiguration configuration,
CsvParserService<CandidatoCSV> candidatoParserService,
CsvParserService<BemCandidatoCSV> bemCandidatoParserService,
CsvParserService<RedeSocialCSV> redeSocialParserService)
CsvParserService<RedeSocialCSV> redeSocialParserService,
CsvParserService<DespesasCSV> despesaParserService,
CsvParserService<ReceitasCSV> receitaParserService,
DespesaReceitaService despesaReceitaService,
ViewRepository viewRepository)
{
this.logger = logger;
this.csvSettings = csvSettings.Value;
@@ -32,6 +44,10 @@ namespace OpenCand.Parser
this.candidatoParserService = candidatoParserService;
this.bemCandidatoParserService = bemCandidatoParserService;
this.redeSocialParserService = redeSocialParserService;
this.despesaParserService = despesaParserService;
this.receitaParserService = receitaParserService;
this.despesaReceitaService = despesaReceitaService;
this.viewRepository = viewRepository;
// Get the base path from either SampleFolder in csvSettings or the BasePath in configuration
BasePath = configuration.GetValue<string>("BasePath") ?? string.Empty;
@@ -49,12 +65,25 @@ namespace OpenCand.Parser
var candidatosDirectory = Path.Combine(BasePath, csvSettings.CandidatosFolder);
var bensCandidatosDirectory = Path.Combine(BasePath, csvSettings.BensCandidatosFolder);
var redesSociaisDirectory = Path.Combine(BasePath, csvSettings.RedesSociaisFolder);
var despesasDirectory = Path.Combine(BasePath, csvSettings.DespesaCandidatoFolder);
var receitasDirectory = Path.Combine(BasePath, csvSettings.ReceitaCandidatoFolder);
await ParseFolder(candidatosDirectory, candidatoParserService);
await ParseFolder(bensCandidatosDirectory, bemCandidatoParserService);
await ParseFolder(redesSociaisDirectory, redeSocialParserService);
await despesaReceitaService.DeleteDespesaAsync();
await ParseFolder(despesasDirectory, despesaParserService);
await despesaReceitaService.DeleteReceitaAsync();
await ParseFolder(receitasDirectory, receitaParserService);
logger.LogInformation("ParseFullDataAsync - Full data parsing completed!");
logger.LogInformation("ParseFullDataAsync - Will refresh materialized views and re-run the analyzes.");
await viewRepository.RefreshMaterializedViews();
logger.LogInformation("ParseFullDataAsync - Materialized views refreshed successfully!");
}
private async Task ParseFolder<CsvObj>(string csvDirectory, CsvParserService<CsvObj> csvParserService)

View File

@@ -5,6 +5,7 @@ using OpenCand.ETL.Contracts;
using OpenCand.Parser.Models;
using OpenCand.Services;
using OpenCand.Parser.Services;
using OpenCand.ETL.Extensions;
namespace OpenCand.ETL.Parser.ParserServices
{
@@ -25,7 +26,7 @@ namespace OpenCand.ETL.Parser.ParserServices
{
// Parse decimal value
decimal? valor = null;
if (!string.IsNullOrEmpty(record.ValorBemCandidato))
if (!record.ValorBemCandidato.IsNullOrEmpty())
{
string normalizedValue = record.ValorBemCandidato.Replace(".", "").Replace(",", ".");
if (decimal.TryParse(normalizedValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedValue))

View File

@@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using OpenCand.Core.Models;
using OpenCand.ETL.Contracts;
using OpenCand.ETL.Extensions;
using OpenCand.Parser.Models;
using OpenCand.Services;
@@ -22,60 +23,79 @@ namespace OpenCand.ETL.Parser.ParserServices
public async Task ParseObject(CandidatoCSV record)
{
if (string.IsNullOrWhiteSpace(record.CPFCandidato) || record.CPFCandidato.Length <= 3)
if (record.CPFCandidato?.Length <= 3 || record.CPFCandidato.IsNullOrEmpty())
{
record.CPFCandidato = null; // Handle null/empty/whitespace CPF
}
else
{
record.CPFCandidato = record.CPFCandidato.Trim();
}
if (record.NomeCandidato == "NÃO DIVULGÁVEL" ||
string.IsNullOrEmpty(record.NomeCandidato) ||
record.NomeCandidato == "#NULO")
if (record.Apelido.IsNullOrEmpty())
{
record.Apelido = null;
}
if (record.NomeCandidato.IsNullOrEmpty())
{
logger.LogCritical($"ParseCandidatosAsync - Candidate with id {record.SequencialCandidato} with invalid name, skipping...");
return; // Skip candidates with invalid name
}
if (string.IsNullOrWhiteSpace(record.Apelido) ||
record.Apelido.Contains("#NUL") ||
record.Apelido.Contains("NULO#") ||
record.Apelido.Contains("#NE"))
if (record.Apelido.IsNullOrEmpty())
{
record.Apelido = null;
}
if (record.SituacaoTurno.IsNullOrEmpty())
{
record.SituacaoTurno = null;
}
var candidato = new Candidato
{
Cpf = record.CPFCandidato,
SqCandidato = record.SequencialCandidato,
Nome = record.NomeCandidato,
Apelido = record.Apelido,
Email = record.Email.Contains("@") ? record.Email : null,
Sexo = record.Genero,
EstadoCivil = record.EstadoCivil,
Escolaridade = record.GrauInstrucao,
Ocupacao = record.Ocupacao,
Nome = record.NomeCandidato.Trim(),
Apelido = record.Apelido?.Trim(),
Sexo = record.Genero.Trim(),
Localidade = record.NomeUE.Trim(),
Ultimoano = record.AnoEleicao,
Eleicoes = new List<CandidatoMapping>()
{
new CandidatoMapping
{
Cpf = record.CPFCandidato,
Nome = record.NomeCandidato,
Apelido = record.Apelido,
SqCandidato = record.SequencialCandidato,
Nome = record.NomeCandidato.Trim(),
SqCandidato = record.SequencialCandidato.Trim(),
Ano = record.AnoEleicao,
TipoEleicao = record.TipoAbrangencia,
NomeUE = record.NomeUE,
SiglaUF = record.SiglaUF,
Cargo = record.DescricaoCargo,
NrCandidato = record.NumeroCandidato,
Resultado = record.SituacaoTurno,
Turno = record.Turno.Trim(),
TipoEleicao = record.TipoAbrangencia.Trim(),
NomeUE = record.NomeUE.Trim(),
SiglaUF = record.SiglaUF.Trim(),
Cargo = record.DescricaoCargo.Trim(),
NrCandidato = record.NumeroCandidato.Trim(),
Resultado = record.SituacaoTurno?.Trim() ?? "-",
Partido = new Partido
{
Sigla = record.SiglaPartido,
Nome = record.NomePartido,
Sigla = record.SiglaPartido.Trim(),
Nome = record.NomePartido.Trim(),
Numero = record.NumeroPartido,
}
}
},
CandidatoExt = new List<CandidatoExt>()
{
new CandidatoExt
{
Apelido = record.Apelido?.Trim(),
EstadoCivil = record.EstadoCivil.Trim(),
Escolaridade = record.GrauInstrucao.Trim(),
Ocupacao = record.Ocupacao.Trim(),
Ano = record.AnoEleicao,
Email = record.Email.IsNullOrEmpty() ? null : record.Email.Trim()
}
}
};

View File

@@ -0,0 +1,88 @@
using Microsoft.Extensions.Logging;
using OpenCand.Core.Models;
using OpenCand.ETL.Contracts;
using OpenCand.ETL.Extensions;
using OpenCand.ETL.Services;
using OpenCand.Parser.Models;
namespace OpenCand.ETL.Parser.ParserServices
{
public class DespesaParserService : IParserService<DespesasCSV>
{
private readonly ILogger<DespesaParserService> logger;
private readonly DespesaReceitaService despesaReceitaService;
public DespesaParserService(
ILogger<DespesaParserService> logger,
DespesaReceitaService despesaReceitaService)
{
this.logger = logger;
this.despesaReceitaService = despesaReceitaService;
}
public async Task ParseObject(DespesasCSV record)
{
var despesa = new Despesa
{
SgPartido = record.SiglaPartido,
Ano = record.AnoEleicao,
Turno = int.Parse(record.Turno),
Descricao = record.DescricaoDespesa,
OrigemDespesa = record.OrigemDespesa,
MunicipioFornecedor = record.NomeMunicipioFornecedor,
NomeFornecedor = record.NomeFornecedor,
NomeFornecedorRFB = record.NomeFornecedorRFB,
SqCandidato = record.SequencialCandidato,
TipoDocumento = record.TipoDocumento,
TipoFornecedor = record.TipoFornecedor,
Valor = record.ValorDespesaContratada / 100
};
if (DateTime.TryParse(record.DataDespesa, out var dataDespesa))
{
despesa.DataDespesa = dataDespesa;
}
else
{
despesa.DataDespesa = null;
}
if (record.CpfCnpjFornecedor.Length == 0 || record.CpfCnpjFornecedor == "-4")
{
despesa.CpfFornecedor = null;
despesa.CnpjFornecedor = null;
}
else if (record.CpfCnpjFornecedor.Length == 11)
{
despesa.CpfFornecedor = record.CpfCnpjFornecedor;
despesa.CnpjFornecedor = null;
}
else if (record.CpfCnpjFornecedor.Length == 14)
{
despesa.CnpjFornecedor = record.CpfCnpjFornecedor;
despesa.CpfFornecedor = null;
}
if (despesa.Descricao.IsNullOrEmpty())
despesa.Descricao = null;
if (despesa.OrigemDespesa.IsNullOrEmpty())
despesa.OrigemDespesa = null;
if (despesa.MunicipioFornecedor.IsNullOrEmpty())
despesa.MunicipioFornecedor = null;
if (despesa.NomeFornecedor.IsNullOrEmpty())
despesa.NomeFornecedor = null;
if (despesa.NomeFornecedorRFB.IsNullOrEmpty())
despesa.NomeFornecedorRFB = null;
if (despesa.TipoDocumento.IsNullOrEmpty())
despesa.TipoDocumento = null;
if (despesa.CpfFornecedor.IsNullOrEmpty())
despesa.CpfFornecedor = null;
if (despesa.CnpjFornecedor.IsNullOrEmpty())
despesa.CnpjFornecedor = null;
if (despesa.TipoFornecedor.IsNullOrEmpty())
despesa.TipoFornecedor = null;
await despesaReceitaService.AddDespesaAsync(despesa);
}
}
}

View File

@@ -0,0 +1,84 @@
using Microsoft.Extensions.Logging;
using OpenCand.Core.Models;
using OpenCand.ETL.Contracts;
using OpenCand.ETL.Extensions;
using OpenCand.ETL.Services;
using OpenCand.Parser.Models;
namespace OpenCand.ETL.Parser.ParserServices
{
public class ReceitaParserService : IParserService<ReceitasCSV>
{
private readonly ILogger<ReceitaParserService> logger;
private readonly DespesaReceitaService despesaReceitaService;
public ReceitaParserService(
ILogger<ReceitaParserService> logger,
DespesaReceitaService despesaReceitaService)
{
this.logger = logger;
this.despesaReceitaService = despesaReceitaService;
}
public async Task ParseObject(ReceitasCSV record)
{
var receita = new Receita
{
EspecieReceita = record.EspecieReceita,
MunicipioDoador = record.NomeMunicipioDoador,
SgPartido = record.SiglaPartido,
FonteReceita = record.FonteReceita,
NaturezaReceita = record.NaturezaReceita,
Ano = record.AnoEleicao,
Descricao = record.DescricaoReceita,
NomeDoador = record.NomeDoador,
NomeDoadorRFB = record.NomeDoadorRFB,
OrigemReceita = record.OrigemReceita,
Turno = int.Parse(record.Turno),
SqCandidato = record.SequencialCandidato,
Valor = record.ValorReceita / 100
};
if (DateTime.TryParse(record.DataReceita, out var dataReceita))
{
receita.DataReceita = dataReceita;
}
else
{
receita.DataReceita = null;
}
if (record.CpfCnpjDoador.Length == 0 || record.CpfCnpjDoador == "-4") {
receita.CpfDoador = null;
receita.CnpjDoador = null;
}
else if (record.CpfCnpjDoador.Length == 11)
{
receita.CpfDoador = record.CpfCnpjDoador;
receita.CnpjDoador = null;
}
else if (record.CpfCnpjDoador.Length == 14)
{
receita.CnpjDoador = record.CpfCnpjDoador;
receita.CpfDoador = null;
}
if (receita.Descricao.IsNullOrEmpty())
receita.Descricao = null;
if (receita.MunicipioDoador.IsNullOrEmpty())
receita.MunicipioDoador = null;
if (receita.NomeDoador.IsNullOrEmpty())
receita.NomeDoador = null;
if (receita.NomeDoadorRFB.IsNullOrEmpty())
receita.NomeDoadorRFB = null;
if (receita.OrigemReceita.IsNullOrEmpty())
receita.OrigemReceita = null;
if (receita.CpfDoador.IsNullOrEmpty())
receita.CpfDoador = null;
if (receita.CnpjDoador.IsNullOrEmpty())
receita.CnpjDoador = null;
await despesaReceitaService.AddReceitaAsync(receita);
}
}
}

View File

@@ -6,6 +6,7 @@ using OpenCand.Config;
using OpenCand.ETL.Contracts;
using OpenCand.ETL.Parser.ParserServices;
using OpenCand.ETL.Repository;
using OpenCand.ETL.Services;
using OpenCand.Parser;
using OpenCand.Parser.Models;
using OpenCand.Parser.Services;
@@ -60,18 +61,25 @@ namespace OpenCand
services.AddTransient<IParserService<CandidatoCSV>, CandidatoParserService>();
services.AddTransient<IParserService<BemCandidatoCSV>, BemCandidatoParserService>();
services.AddTransient<IParserService<RedeSocialCSV>, RedeSocialParserService>();
services.AddTransient<IParserService<DespesasCSV>, DespesaParserService>();
services.AddTransient<IParserService<ReceitasCSV>, ReceitaParserService>();
services.AddTransient<CsvParserService<CandidatoCSV>>();
services.AddTransient<CsvParserService<BemCandidatoCSV>>();
services.AddTransient<CsvParserService<RedeSocialCSV>>();
services.AddTransient<CsvParserService<DespesasCSV>>();
services.AddTransient<CsvParserService<ReceitasCSV>>();
services.AddTransient<ParserManager>();
services.AddTransient<DespesaReceitaService>();
services.AddTransient<CandidatoService>();
services.AddTransient<BemCandidatoService>();
services.AddTransient<RedeSocialService>();
services.AddTransient<DespesaReceitaRepository>();
services.AddTransient<CandidatoRepository>();
services.AddTransient<BemCandidatoRepository>();
services.AddTransient<RedeSocialRepository>();
services.AddTransient<PartidoRepository>();
services.AddTransient<CsvFixerService>();
services.AddTransient<ViewRepository>();
});
}
}

View File

@@ -16,18 +16,17 @@ namespace OpenCand.Repository
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO candidato (idcandidato, cpf, nome, apelido, datanascimento, email, sexo, estadocivil, escolaridade, ocupacao)
VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @email, @sexo, @estadocivil, @escolaridade, @ocupacao)
INSERT INTO candidato (idcandidato, cpf, nome, apelido, datanascimento, sexo, ultimoano, localidade)
VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @sexo, @ultimoano, @localidade)
ON CONFLICT (idcandidato) DO UPDATE SET
cpf = EXCLUDED.cpf,
nome = EXCLUDED.nome,
apelido = EXCLUDED.apelido,
datanascimento = EXCLUDED.datanascimento,
email = EXCLUDED.email,
sexo = EXCLUDED.sexo,
estadocivil = EXCLUDED.estadocivil,
escolaridade = EXCLUDED.escolaridade,
ocupacao = EXCLUDED.ocupacao,
apelido = EXCLUDED.apelido;",
localidade = EXCLUDED.localidade,
ultimoano = EXCLUDED.ultimoano
WHERE candidato.ultimoano IS NULL OR EXCLUDED.ultimoano > candidato.ultimoano;",
new
{
idcandidato = candidato.IdCandidato,
@@ -35,11 +34,9 @@ namespace OpenCand.Repository
nome = candidato.Nome,
apelido = candidato.Apelido,
datanascimento = candidato.DataNascimento,
email = candidato.Email,
sexo = candidato.Sexo,
estadocivil = candidato.EstadoCivil,
escolaridade = candidato.Escolaridade,
ocupacao = candidato.Ocupacao
localidade = candidato.Localidade,
ultimoano = candidato.Ultimoano
});
}
}
@@ -49,17 +46,17 @@ namespace OpenCand.Repository
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO candidato_mapping (idcandidato, cpf, nome, apelido, sqcandidato, ano, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado)
VALUES (@idcandidato, @cpf, @nome, @apelido, @sqcandidato, @ano, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado)
INSERT INTO candidato_mapping (idcandidato, cpf, nome, sqcandidato, ano, turno, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado)
VALUES (@idcandidato, @cpf, @nome, @sqcandidato, @ano, @turno, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado)
ON CONFLICT DO NOTHING;",
new
{
idcandidato = candidatoMapping.IdCandidato,
cpf = candidatoMapping.Cpf,
nome = candidatoMapping.Nome,
apelido = candidatoMapping.Apelido,
sqcandidato = candidatoMapping.SqCandidato,
ano = candidatoMapping.Ano,
turno = candidatoMapping.Turno,
tipoeleicao = candidatoMapping.TipoEleicao,
siglauf = candidatoMapping.SiglaUF,
nomeue = candidatoMapping.NomeUE,
@@ -71,6 +68,32 @@ namespace OpenCand.Repository
}
}
public async Task AddCandidatoExtAsync(CandidatoExt candidatoExt)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO candidato_ext (idcandidato, ano, apelido, email, estadocivil, escolaridade, ocupacao)
VALUES (@idcandidato, @ano, @apelido, @email, @estadocivil, @escolaridade, @ocupacao)
ON CONFLICT (idcandidato, ano) DO UPDATE SET
apelido = EXCLUDED.apelido,
email = EXCLUDED.email,
estadocivil = EXCLUDED.estadocivil,
escolaridade = EXCLUDED.escolaridade,
ocupacao = EXCLUDED.ocupacao;",
new
{
idcandidato = candidatoExt.IdCandidato,
ano = candidatoExt.Ano,
apelido = candidatoExt.Apelido,
email = candidatoExt.Email,
estadocivil = candidatoExt.EstadoCivil,
escolaridade = candidatoExt.Escolaridade,
ocupacao = candidatoExt.Ocupacao
});
}
}
public async Task<Candidato?> GetCandidatoByCpf(string cpf)
{
using (var connection = new NpgsqlConnection(ConnectionString))
@@ -83,6 +106,18 @@ namespace OpenCand.Repository
}
}
public async Task<Guid?> GetIdCandidatoBySqCandidato(string sqcandidato)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
var query = @"
SELECT idcandidato
FROM candidato_mapping
WHERE sqcandidato = @sqcandidato";
return await connection.QueryFirstOrDefaultAsync<Guid>(query, new { sqcandidato });
}
}
public async Task<Candidato?> GetCandidatoByNome(string nome, DateTime datanascimento)
{
using (var connection = new NpgsqlConnection(ConnectionString))

View File

@@ -0,0 +1,155 @@
using Dapper;
using Microsoft.Extensions.Configuration;
using Npgsql;
using OpenCand.Core.Models;
namespace OpenCand.Repository
{
public class DespesaReceitaRepository : BaseRepository
{
public DespesaReceitaRepository(IConfiguration configuration) : base(configuration)
{
}
public async Task AddDespesaAsync(Despesa despesa)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO despesas_candidato (
idcandidato,
ano,
turno,
sqcandidato,
sgpartido,
tipofornecedor,
cnpjfornecedor,
cpffornecedor,
nomefornecedor,
nomefornecedorrfb,
municipiofornecedor,
tipodocumento,
datadespesa,
descricao,
origemdespesa,
valor
) VALUES (
@idCandidato,
@ano,
@turno,
@sqCandidato,
@sgPartido,
@tipoFornecedor,
@cnpjFornecedor,
@cpfFornecedor,
@nomeFornecedor,
@nomeFornecedorRFB,
@municipioFornecedor,
@tipoDocumento,
@dataDespesa,
@descricao,
@origemdespesa,
@valor
)", new
{
idCandidato = despesa.IdCandidato,
ano = despesa.Ano,
turno = despesa.Turno,
sqCandidato = despesa.SqCandidato,
sgPartido = despesa.SgPartido,
tipoFornecedor = despesa.TipoFornecedor,
cnpjFornecedor = despesa.CnpjFornecedor,
cpfFornecedor = despesa.CpfFornecedor,
nomeFornecedor = despesa.NomeFornecedor,
nomeFornecedorRFB = despesa.NomeFornecedorRFB,
municipioFornecedor = despesa.MunicipioFornecedor,
tipoDocumento = despesa.TipoDocumento,
dataDespesa = despesa.DataDespesa,
descricao = despesa.Descricao,
origemdespesa = despesa.OrigemDespesa,
valor = despesa.Valor
});
}
}
public async Task AddReceitaAsync(Receita receita)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO receitas_candidato (
idcandidato,
ano,
turno,
sqcandidato,
sgpartido,
fontereceita,
origemreceita,
naturezareceita,
especiereceita,
cnpjdoador,
cpfdoador,
nomedoador,
nomedoadorrfb,
municipiodoador,
datareceita,
descricao,
valor
) VALUES (
@idCandidato,
@ano,
@turno,
@sqCandidato,
@sgPartido,
@fonteReceita,
@origemReceita,
@naturezaReceita,
@especieReceita,
@cnpjDoador,
@cpfDoador,
@nomeDoador,
@nomeDoadorRFB,
@municipioDoador,
@dataReceita,
@descricao,
@valor
)", new
{
idCandidato = receita.IdCandidato,
ano = receita.Ano,
turno = receita.Turno,
sqCandidato = receita.SqCandidato,
sgPartido = receita.SgPartido,
fonteReceita = receita.FonteReceita,
origemReceita = receita.OrigemReceita,
naturezaReceita = receita.NaturezaReceita,
especieReceita = receita.EspecieReceita,
cnpjDoador = receita.CnpjDoador,
cpfDoador = receita.CpfDoador,
nomeDoador = receita.NomeDoador,
nomeDoadorRFB = receita.NomeDoadorRFB,
municipioDoador = receita.MunicipioDoador,
dataReceita = receita.DataReceita,
descricao = receita.Descricao,
valor = receita.Valor
});
}
}
public async Task DeleteDespesaAsync()
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync("DELETE FROM despesas_candidato;");
}
}
public async Task DeleteReceitaAsync()
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync("DELETE FROM receitas_candidato;");
}
}
}
}

View File

@@ -0,0 +1,36 @@
using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Npgsql;
using OpenCand.Core.Models;
using OpenCand.Repository;
namespace OpenCand.ETL.Repository
{
public class ViewRepository : BaseRepository
{
public ViewRepository(IConfiguration configuration) : base(configuration)
{ }
public async Task RefreshMaterializedViews()
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
// Get all materialized view names
var materializedViews = await connection.QueryAsync<string>(
@"SELECT schemaname || '.' || matviewname as full_name
FROM pg_matviews
ORDER BY schemaname, matviewname");
foreach (var viewName in materializedViews)
{
// Refresh the materialized view
await connection.ExecuteAsync($"REFRESH MATERIALIZED VIEW {viewName}");
// Analyze the materialized view to update statistics
await connection.ExecuteAsync($"ANALYZE {viewName}");
}
}
}
}
}

View File

@@ -17,7 +17,7 @@ namespace OpenCand.Services
public async Task AddCandidatoAsync(Candidato candidato)
{
if (candidato == null)
if (candidato == null || candidato.CandidatoExt == null || candidato.Eleicoes == null)
{
throw new ArgumentNullException(nameof(candidato), "Candidato cannot be null");
}
@@ -28,6 +28,7 @@ namespace OpenCand.Services
}
var candidatoMapping = candidato.Eleicoes.First();
var candidatoExt = candidato.CandidatoExt.First();
// Add partido data
if (candidatoMapping.Partido != null)
@@ -52,18 +53,18 @@ namespace OpenCand.Services
candidato.IdCandidato = existingCandidato.IdCandidato;
candidato.Cpf = GetNonEmptyString(existingCandidato.Cpf, candidato.Cpf);
candidato.Email = GetNonEmptyString(existingCandidato.Email, candidato.Email);
candidato.EstadoCivil = GetNonEmptyString(existingCandidato.EstadoCivil, candidato.EstadoCivil);
candidato.Apelido = GetNonEmptyString(existingCandidato.Apelido, candidato.Apelido);
candidato.Escolaridade = GetNonEmptyString(existingCandidato.Escolaridade, candidato.Escolaridade);
candidato.Ocupacao = GetNonEmptyString(existingCandidato.Ocupacao, candidato.Ocupacao);
candidato.Sexo = GetNonEmptyString(existingCandidato.Sexo, candidato.Sexo);
candidatoMapping.IdCandidato = candidato.IdCandidato;
candidatoMapping.Cpf = GetNonEmptyString(candidato.Cpf, candidatoMapping.Cpf);
candidatoExt.IdCandidato = candidato.IdCandidato;
// Update the entries for the existing candidate
await candidatoRepository.AddCandidatoAsync(candidato);
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
return;
}
@@ -79,10 +80,12 @@ namespace OpenCand.Services
if (existingMapping != null)
{
candidato.IdCandidato = existingMapping.IdCandidato;
candidato.Cpf = GetNonEmptyString(existingMapping.Cpf, candidato.Cpf);
candidato.Apelido = GetNonEmptyString(existingMapping.Apelido, candidato.Apelido);
await candidatoRepository.AddCandidatoAsync(candidato);
candidatoExt.IdCandidato = existingMapping.IdCandidato;
candidato.Cpf = GetNonEmptyString(existingMapping.Cpf, candidato.Cpf);
await candidatoRepository.AddCandidatoAsync(candidato);
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
return;
}
@@ -94,8 +97,11 @@ namespace OpenCand.Services
candidatoMapping.Cpf = candidato.Cpf;
candidatoMapping.Nome = candidato.Nome;
candidatoExt.IdCandidato = candidato.IdCandidato;
await candidatoRepository.AddCandidatoAsync(candidato);
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
}
public string GetNonEmptyString(string? value1, string? value2)

View File

@@ -0,0 +1,64 @@
using OpenCand.Core.Models;
using OpenCand.Repository;
namespace OpenCand.ETL.Services
{
public class DespesaReceitaService
{
private readonly DespesaReceitaRepository despesaReceitaRepository;
private readonly CandidatoRepository candidatorepository;
public DespesaReceitaService(DespesaReceitaRepository despesaReceitaRepository, CandidatoRepository candidatoRepository)
{
this.despesaReceitaRepository = despesaReceitaRepository;
this.candidatorepository = candidatoRepository;
}
public async Task AddDespesaAsync(Despesa despesa)
{
if (despesa == null || string.IsNullOrEmpty(despesa.SqCandidato))
{
throw new ArgumentNullException(nameof(despesa), "Despesa cannot be null");
}
var idCandidato = await candidatorepository.GetIdCandidatoBySqCandidato(despesa.SqCandidato);
if (idCandidato == Guid.Empty)
{
throw new ArgumentException($"Candidato with SqCandidato {despesa.SqCandidato} not found.");
}
despesa.IdCandidato = (Guid)idCandidato;
await despesaReceitaRepository.AddDespesaAsync(despesa);
}
public async Task AddReceitaAsync(Receita receita)
{
if (receita == null || string.IsNullOrEmpty(receita.SqCandidato))
{
throw new ArgumentNullException(nameof(receita), "Receita cannot be null");
}
var idCandidato = await candidatorepository.GetIdCandidatoBySqCandidato(receita.SqCandidato);
if (idCandidato == Guid.Empty)
{
throw new ArgumentException($"Candidato with SqCandidato {receita.SqCandidato} not found.");
}
receita.IdCandidato = (Guid)idCandidato;
await despesaReceitaRepository.AddReceitaAsync(receita);
}
public async Task DeleteDespesaAsync()
{
await despesaReceitaRepository.DeleteDespesaAsync();
}
public async Task DeleteReceitaAsync()
{
await despesaReceitaRepository.DeleteReceitaAsync();
}
}
}

View File

@@ -30,7 +30,7 @@ namespace OpenCand.Services
}
redeSocial.IdCandidato = candidato.IdCandidato;
redeSocial.Rede = GetRedeSocialType(redeSocial.Link);
redeSocial.Rede = GetRedeSocialType(redeSocial.Link.Trim());
await redeSocialRepository.AddRedeSocialAsync(redeSocial);
}

View File

@@ -12,11 +12,15 @@
"CsvSettings": {
"CandidatosFolder": "data/consulta_cand",
"BensCandidatosFolder": "data/bem_candidato",
"RedesSociaisFolder": "data/rede_social"
"RedesSociaisFolder": "data/rede_social",
"DespesaCandidatoFolder": "data/despesas_candidato",
"ReceitaCandidatoFolder": "data/receitas_candidato"
},
"ParserSettings": {
"DefaultThreads": 40,
"CandidatoCSVThreads": 5
"CandidatoCSVThreads": 40,
"DepesasCSVThreads": 50,
"ReceitasCSVThreads": 50
},
"BasePath": "sample"
}

View File

@@ -9,3 +9,90 @@ OpenCand is built using:
* .NET Core 8 - for the API
* PostgreSQL - for the database
* React - for the front-end
## Disponibilidade de dados (Prod)
* ✅ = Disponível
* ❌ = Não disponível
* ⛔ = Sem dados
| Nome do dado | Ano | Disponível? |
|-------------------|------|-------------|
| Candidatos | 2024 | ✅ |
| Bem Candidato | 2024 | ✅ |
| Despesas/Receitas | 2024 | ✅ |
| Rede Social | 2024 | ✅ |
| Fotos | 2024 | ✅ |
| - | - | - |
| Candidatos | 2022 | ✅ |
| Bem Candidato | 2022 | ✅ |
| Despesas/Receitas | 2022 | ✅ |
| Rede Social | 2022 | ✅ |
| Fotos | 2022 | ✅ |
| - | - | - |
| Candidatos | 2020 | ✅ |
| Bem Candidato | 2020 | ✅ |
| Despesas/Receitas | 2020 | ✅ |
| Rede Social | 2020 | ✅ |
| Fotos | 2020 | ❌ |
| - | - | - |
| Candidatos | 2018 | ✅ |
| Bem Candidato | 2018 | ✅ |
| Despesas/Receitas | 2018 | ❌ |
| Rede Social | 2018 | ❌ |
| Fotos | 2018 | ❌ |
| - | - | - |
| Candidatos | 2016 | ✅ |
| Bem Candidato | 2016 | ✅ |
| Despesas/Receitas | 2016 | ❌ |
| Rede Social | 2016 | ❌ |
| Fotos | 2016 | ❌ |
| - | - | - |
| Candidatos | 2014 | ✅ |
| Bem Candidato | 2014 | ✅ |
| Despesas/Receitas | 2014 | ❌ |
| Rede Social | 2014 | ❌ |
| Fotos | 2014 | ❌ |
| - | - | - |
| Candidatos | 2012 | ✅ |
| Bem Candidato | 2012 | ✅ |
| Despesas/Receitas | 2012 | ❌ |
| Rede Social | 2012 | ❌ |
| Fotos | 2012 | ❌ |
| - | - | - |
| Candidatos | 2010 | ✅ |
| Bem Candidato | 2010 | ✅ |
| Despesas/Receitas | 2010 | ❌ |
| Rede Social | 2010 | ❌ |
| Fotos | 2010 | ❌ |
| - | - | - |
| Candidatos | 2008 | ✅ |
| Bem Candidato | 2008 | ✅ |
| Despesas/Receitas | 2008 | ❌ |
| Rede Social | 2008 | ❌ |
| Fotos | 2008 | ❌ |
| - | - | - |
| Candidatos | 2006 | ✅ |
| Bem Candidato | 2006 | ✅ |
| Despesas/Receitas | 2006 | ❌ |
| Rede Social | 2006 | ❌ |
| Fotos | 2006 | ❌ |
| - | - | - |
| Candidatos | 2004 | ✅ |
| Bem Candidato | 2004 | ❌ |
| Despesas/Receitas | 2004 | ❌ |
| Rede Social | 2004 | ❌ |
| Fotos | 2004 | ❌ |
| - | - | - |
| Candidatos | 2002 | ✅ |
| Bem Candidato | 2002 | ❌ |
| Despesas/Receitas | 2002 | ❌ |
| Rede Social | 2002 | ❌ |
| Fotos | 2002 | ❌ |
| - | - | - |
| Candidatos | 2000 | ✅ |
| Bem Candidato | 2000 | ⛔ |
| Despesas/Receitas | 2000 | ⛔ |
| Rede Social | 2000 | ⛔ |
| Fotos | 2000 | ⛔ |
| - | - | - |

View File

@@ -1,31 +1,36 @@
DROP TABLE IF EXISTS bem_candidato CASCADE;
DROP TABLE IF EXISTS candidato_mapping CASCADE;
DROP TABLE IF EXISTS candidato_ext CASCADE;
DROP TABLE IF EXISTS rede_social CASCADE;
DROP TABLE IF EXISTS candidato CASCADE;
DROP TABLE IF EXISTS partido CASCADE;
DROP TABLE IF EXISTS despesas_candidato CASCADE;
DROP TABLE IF EXISTS receitas_candidato CASCADE;
CREATE TABLE candidato (
idcandidato UUID NOT NULL PRIMARY KEY,
cpf VARCHAR(11),
nome VARCHAR(255) NOT NULL,
apelido VARCHAR(255),
datanascimento TIMESTAMPTZ,
email TEXT,
sexo CHAR(15),
estadocivil VARCHAR(50),
escolaridade VARCHAR(50),
ocupacao VARCHAR(150)
apelido VARCHAR(255),
localidade VARCHAR(100),
ultimoano INT,
popularidade BIGINT DEFAULT 0
);
CREATE INDEX idx_candidato_nome ON candidato (nome);
CREATE INDEX idx_candidato_apelido ON candidato (apelido);
CREATE INDEX idx_candidato_datanascimento ON candidato (datanascimento);
CREATE INDEX idx_candidato_nomenascimento ON candidato (nome, datanascimento);
CREATE INDEX idx_candidato_cpf ON candidato (cpf);
-- Each candidato (idcandidato, cpf, nome) will be mapped to a (sqcandidato, ano, tipo_eleicao, sg_uf, cargo, resultado)
CREATE TABLE candidato_mapping (
idcandidato UUID NOT NULL,
cpf VARCHAR(11),
nome VARCHAR(255) NOT NULL,
apelido VARCHAR(255),
sqcandidato TEXT,
sqcandidato VARCHAR(50) NOT NULL,
turno VARCHAR(2) NOT NULL,
ano INT NOT NULL,
tipoeleicao VARCHAR(50),
siglauf VARCHAR(2),
@@ -37,12 +42,25 @@ CREATE TABLE candidato_mapping (
CONSTRAINT pk_candidato_mapping PRIMARY KEY (idcandidato, ano, siglauf, nomeue, cargo, nrcandidato, resultado),
CONSTRAINT fk_candidato_mapping_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX idx_candidato_mapping_idcandidato ON candidato_mapping (idcandidato);
CREATE INDEX idx_candidato_mapping_cpf ON candidato_mapping (cpf);
CREATE INDEX idx_candidato_mapping_nome ON candidato_mapping (nome);
CREATE INDEX idx_candidato_mapping_apelido ON candidato_mapping (apelido);
CREATE INDEX idx_candidato_mapping_ano ON candidato_mapping (ano);
CREATE INDEX idx_candidato_mapping_sqcandidato ON candidato_mapping (sqcandidato);
CREATE TABLE candidato_ext (
idcandidato UUID NOT NULL,
ano INT NOT NULL,
apelido VARCHAR(255),
email TEXT,
estadocivil VARCHAR(50),
escolaridade VARCHAR(50),
ocupacao TEXT,
CONSTRAINT pk_candidato_ext PRIMARY KEY (idcandidato, ano)
);
CREATE INDEX idx_candidato_ext_idcandidato ON candidato_ext (idcandidato);
CREATE INDEX idx_candidato_ext_ano ON candidato_ext (ano);
---- Table for storing assets of candidates
CREATE TABLE bem_candidato (
idcandidato UUID NOT NULL,
@@ -77,3 +95,65 @@ CREATE TABLE partido (
);
CREATE INDEX idx_partido_nome ON partido (nome);
CREATE INDEX idx_partido_numero ON partido (numero);
---- Tables for storing despesas e receitas of candidacies
CREATE TABLE despesas_candidato (
iddespesa UUID NOT NULL DEFAULT gen_random_uuid(),
idcandidato UUID NOT NULL,
ano INT NOT NULL,
turno VARCHAR(2) NOT NULL,
sqcandidato VARCHAR(50) NOT NULL,
sgpartido VARCHAR(50) NOT NULL,
tipofornecedor VARCHAR(150),
cnpjfornecedor VARCHAR(14),
cpffornecedor VARCHAR(11),
nomefornecedor VARCHAR(255),
nomefornecedorrfb VARCHAR(255),
municipiofornecedor VARCHAR(100),
tipodocumento VARCHAR(50),
datadespesa TIMESTAMPTZ,
descricao TEXT,
origemdespesa TEXT,
valor NUMERIC(20, 2),
CONSTRAINT pk_despesas_candidato PRIMARY KEY (iddespesa),
CONSTRAINT fk_despesas_candidato_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX idx_despesas_candidato_idcandidato ON despesas_candidato (idcandidato);
CREATE INDEX idx_despesas_candidato_ano ON despesas_candidato (ano);
CREATE INDEX idx_despesas_candidato_sqcandidato ON despesas_candidato (sqcandidato);
CREATE INDEX idx_despesas_candidato_sgpartido ON despesas_candidato (sgpartido);
CREATE TABLE receitas_candidato (
idreceita UUID NOT NULL DEFAULT gen_random_uuid(),
idcandidato UUID NOT NULL,
ano INT NOT NULL,
turno VARCHAR(2) NOT NULL,
sqcandidato VARCHAR(50) NOT NULL,
sgpartido VARCHAR(50) NOT NULL,
fontereceita VARCHAR(150),
origemreceita VARCHAR(250),
naturezareceita VARCHAR(250),
especiereceita VARCHAR(250),
cnpjdoador VARCHAR(14),
cpfdoador VARCHAR(11),
nomedoador VARCHAR(255),
nomedoadorrfb VARCHAR(255),
municipiodoador VARCHAR(100),
datareceita TIMESTAMPTZ,
descricao TEXT,
valor NUMERIC(20, 2),
CONSTRAINT pk_receitas_candidato PRIMARY KEY (idreceita),
CONSTRAINT fk_receitas_candidato_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX idx_receitas_candidato_idcandidato ON receitas_candidato (idcandidato);
CREATE INDEX idx_receitas_candidato_ano ON receitas_candidato (ano);
CREATE INDEX idx_receitas_candidato_sqcandidato ON receitas_candidato (sqcandidato);
CREATE INDEX idx_receitas_candidato_sgpartido ON receitas_candidato (sgpartido);
-- Search function
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_candidato_nome_trgm ON candidato USING GIN (nome gin_trgm_ops);
CREATE INDEX idx_candidato_apelido_trgm ON candidato USING GIN (apelido gin_trgm_ops);
CREATE INDEX idx_candidato_cpf_trgm ON candidato USING GIN (cpf gin_trgm_ops);

92
db/mv.sql Normal file
View File

@@ -0,0 +1,92 @@
-- This script creates materialized views for bem_candidato, despesas_candidato, and receitas_candidato
-- Drop existing materialized views if they exist
DROP MATERIALIZED VIEW IF EXISTS mv_bem_candidato;
DROP MATERIALIZED VIEW IF EXISTS mv_despesas_candidato;
DROP MATERIALIZED VIEW IF EXISTS mv_receitas_candidato;
-- Creation stage
---
--- MV for bem_candidato
CREATE MATERIALIZED VIEW mv_bem_candidato AS
SELECT
bem_candidato.ano,
bem_candidato.idcandidato,
candidato_mapping.siglauf as siglauf,
candidato_mapping.sgpartido,
cargo,
SUM(valor) AS valor
FROM
bem_candidato
JOIN candidato_mapping ON bem_candidato.idcandidato = candidato_mapping.idcandidato AND bem_candidato.ano = candidato_mapping.ano AND candidato_mapping.turno = '1'
GROUP BY
bem_candidato.ano,
bem_candidato.idcandidato,
candidato_mapping.sgpartido,
siglauf,
cargo;
CREATE INDEX idx_mv_bem_candidato ON mv_bem_candidato (ano, idcandidato, siglauf, sgpartido, cargo);
---
--- MV for despesas_candidato
CREATE MATERIALIZED VIEW mv_despesas_candidato AS
SELECT
despesas_candidato.ano,
despesas_candidato.idcandidato,
candidato_mapping.siglauf as siglauf,
despesas_candidato.sgpartido,
cargo,
SUM(valor) AS valor
FROM
despesas_candidato
JOIN candidato_mapping ON despesas_candidato.idcandidato = candidato_mapping.idcandidato AND despesas_candidato.ano = candidato_mapping.ano AND candidato_mapping.turno = '1'
GROUP BY
despesas_candidato.ano,
despesas_candidato.idcandidato,
despesas_candidato.sgpartido,
siglauf,
cargo;
CREATE INDEX idx_mv_despesas_candidato ON mv_despesas_candidato (ano, idcandidato, siglauf, sgpartido, cargo);
---
--- MV for receitas_candidato
CREATE MATERIALIZED VIEW mv_receitas_candidato AS
SELECT
receitas_candidato.ano,
receitas_candidato.idcandidato,
candidato_mapping.siglauf as siglauf,
receitas_candidato.sgpartido,
cargo,
SUM(valor) AS valor
FROM
receitas_candidato
JOIN candidato_mapping ON receitas_candidato.idcandidato = candidato_mapping.idcandidato AND receitas_candidato.ano = candidato_mapping.ano AND candidato_mapping.turno = '1'
GROUP BY
receitas_candidato.ano,
receitas_candidato.idcandidato,
receitas_candidato.sgpartido,
siglauf,
cargo;
CREATE INDEX idx_mv_receitas_candidato ON mv_receitas_candidato (ano, idcandidato, siglauf, sgpartido, cargo);
---
--- View for candidate mapping summary
CREATE MATERIALIZED VIEW mv_candidato_mapping_analytics AS
SELECT DISTINCT cm.idcandidato, c.nome, cm.ano, cm.turno, cm.sgpartido, cm.siglauf, cm.cargo
FROM candidato_mapping cm
JOIN candidato c ON cm.idcandidato = c.idcandidato
WHERE cm.turno = '1';
CREATE INDEX idx_mv_candidato_mapping_analytics ON mv_candidato_mapping_analytics (idcandidato, ano);
-- Refresh the materialized views to ensure they are up-to-date
REFRESH MATERIALIZED VIEW mv_bem_candidato;
REFRESH MATERIALIZED VIEW mv_despesas_candidato;
REFRESH MATERIALIZED VIEW mv_receitas_candidato;
REFRESH MATERIALIZED VIEW mv_candidato_mapping_analytics;
-- Force re-analyze the materialized views to update statistics
ANALYZE mv_bem_candidato;
ANALYZE mv_despesas_candidato;
ANALYZE mv_receitas_candidato;
ANALYZE mv_candidato_mapping_analytics;