Compare commits

...

32 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
b9908b36b7 adding rate limit to the API
All checks were successful
API and ETL Build / build_etl (push) Successful in 14s
API and ETL Build / build_api (push) Successful in 13s
2025-06-03 17:29:02 -03:00
a7732dfccf add threads config
All checks were successful
API and ETL Build / build_etl (push) Successful in 9s
API and ETL Build / build_api (push) Successful in 2s
2025-06-03 16:40:53 -03:00
2660826a3f stuff and refactor
All checks were successful
API and ETL Build / build_etl (push) Successful in 8s
API and ETL Build / build_api (push) Successful in 9s
2025-06-03 16:27:39 -03:00
03b1f4f1d1 add apelido 2025-06-02 16:47:24 -03:00
a3d67198af partido + melhorias
All checks were successful
API and ETL Build / build_etl (push) Successful in 17s
API and ETL Build / build_api (push) Successful in 9s
2025-05-31 20:46:48 -03:00
146495c07b adding more stuff
All checks were successful
API and ETL Build / build_etl (push) Successful in 2s
API and ETL Build / build_api (push) Successful in 14s
2025-05-31 14:43:37 -03:00
e00cabb840 ignore more stuff 2025-05-31 13:03:06 -03:00
59 changed files with 2992 additions and 589 deletions

3
.gitignore vendored
View File

@@ -12,4 +12,7 @@ sample*
data
appsettings.Development.json
fotos_cand
*.zip
docker-compose.yaml

View File

@@ -0,0 +1,95 @@
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
namespace OpenCand.API.Config
{
public static class RateLimitingConfig
{
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)
{
services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: GetClientIdentifier(httpContext),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 2000, // Global limit per minute
Window = TimeSpan.FromMinutes(1)
}));
// Default policy: 200 requests per minute with burst of 100
options.AddFixedWindowLimiter(policyName: DefaultPolicy, options =>
{
options.PermitLimit = 400;
options.Window = TimeSpan.FromMinutes(1);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 100; // Burst capacity
});
options.AddFixedWindowLimiter(policyName: CandidatoSearchPolicy, options =>
{
options.PermitLimit = 300;
options.Window = TimeSpan.FromMinutes(1);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 200; // Burst capacity
});
options.AddFixedWindowLimiter(policyName: CpfRevealPolicy, options =>
{
options.PermitLimit = 20;
options.Window = TimeSpan.FromMinutes(1);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
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.Append("Retry-After", retryAfter.Value.ToString());
}
await context.HttpContext.Response.WriteAsync(
"Rate limit exceeded. Please try again later.", cancellationToken: token);
};
});
}
private static string GetClientIdentifier(HttpContext httpContext)
{
// Get client IP address for partitioning
var clientIp = httpContext.Connection.RemoteIpAddress?.ToString()
?? httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault()
?? "unknown";
// Include the endpoint in the partition key for endpoint-specific limits
var endpoint = httpContext.Request.Path.ToString();
return $"{clientIp}:{endpoint}";
}
private static int? GetRetryAfter(OnRejectedContext context)
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
return (int)retryAfter.TotalSeconds;
}
return null;
}
}
}

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,7 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using OpenCand.API.Config;
using OpenCand.API.Model;
using OpenCand.API.Services;
using OpenCand.Core.Models;
using OpenCand.Core.Utils;
namespace OpenCand.API.Controllers
{
@@ -15,15 +18,37 @@ namespace OpenCand.API.Controllers
}
[HttpGet("search")]
[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")]
@@ -37,5 +62,27 @@ namespace OpenCand.API.Controllers
{
return await openCandService.GetCandidatoRedeSocialById(id);
}
[HttpGet("{id}/reveal-cpf")]
[EnableRateLimiting(RateLimitingConfig.CpfRevealPolicy)]
public async Task<CpfRevealResult> GetCandidatoCpfById([FromRoute] Guid id)
{
var rnd = new Random();
var randomWait = rnd.Next(1000, 3000);
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

@@ -1,9 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using OpenCand.API.Config;
using OpenCand.API.Services;
using OpenCand.Core.Models;
namespace OpenCand.API.Controllers
{
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
public class StatsController : BaseController
{
private readonly OpenCandService openCandService;
@@ -18,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

@@ -16,4 +16,19 @@ namespace OpenCand.API.Model
{
public List<RedeSocial> RedesSociais { get; set; }
}
public class CpfRevealResult
{
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

@@ -5,10 +5,9 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Npgsql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.3" />
<PackageReference Include="Dapper" Version="2.1.66" />
</ItemGroup>
@@ -25,4 +24,10 @@
</None>
</ItemGroup>
<ItemGroup>
<None Remove="fotos_cand.zip" />
<Content Remove="fotos_cand" />
</ItemGroup>
</Project>

View File

@@ -1,3 +1,4 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using OpenCand.API.Config;
using OpenCand.API.Repository;
@@ -10,13 +11,14 @@ namespace OpenCand.API
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var builder = WebApplication.CreateBuilder(args); // Add services to the container.
builder.Services.AddControllers();
SetupServices(builder);
// Configure rate limiting
builder.Services.ConfigureRateLimiting();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
@@ -35,9 +37,11 @@ namespace OpenCand.API
FileProvider = new PhysicalFileProvider(Path.Combine(workingDir, "fotos_cand")),
RequestPath = "/assets/fotos"
});
app.UseHttpsRedirection();
// Use rate limiting middleware
app.UseRateLimiter();
app.UseAuthorization();
app.MapControllers();
@@ -54,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,31 +7,52 @@ 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 idcandidato, cpf, nome, datanascimento, email, sexo, estadocivil, escolaridade, ocupacao
FROM candidato
WHERE nome ILIKE '%' || @query || '%' OR
cpf ILIKE '%' || @query || '%' OR
email ILIKE '%' || @query || '%'
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))
{
return await connection.QueryFirstOrDefaultAsync<Candidato>(@"
SELECT * FROM candidato
WHERE idcandidato = @idcandidato;",
new { idcandidato });
}
});
}
public async Task<string?> GetCandidatoCpfAsync(Guid idcandidato)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<Candidato>(@"
SELECT * FROM candidato
return await connection.QueryFirstOrDefaultAsync<string>(@"
SELECT cpf FROM candidato
WHERE idcandidato = @idcandidato;",
new { idcandidato });
}
@@ -47,6 +69,17 @@ namespace OpenCand.Repository
}
}
public async Task<Partido?> GetPartidoBySigla(string sigla)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
var query = @"
SELECT * FROM partido
WHERE sigla = @sigla";
return await connection.QueryFirstOrDefaultAsync<Partido>(query, new { sigla });
}
}
public async Task<List<RedeSocial>?> GetCandidatoRedeSocialById(Guid idcandidato)
{
using (var connection = new NpgsqlConnection(ConnectionString))
@@ -57,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,29 +1,143 @@
using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Npgsql;
using OpenCand.Core.Models;
using OpenCand.Repository;
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()
{
using (var connection = new NpgsqlConnection(ConnectionString))
string cacheKey = GenerateCacheKey("OpenCandStats");
var result = await GetOrSetCacheAsync(cacheKey, async () =>
{
var stats = await connection.QueryFirstOrDefaultAsync<OpenCandStats>(@"
SELECT
(SELECT COUNT(idcandidato) FROM candidato) AS TotalCandidatos,
(SELECT COUNT(*) FROM bem_candidato) AS TotalBemCandidatos,
(SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos,
(SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais,
(SELECT COUNT(DISTINCT ano) FROM bem_candidato) AS TotalEleicoes;");
return stats ?? new OpenCandStats();
}
using (var connection = new NpgsqlConnection(ConnectionString))
{
var stats = await connection.QueryFirstOrDefaultAsync<OpenCandStats>(@"
SELECT
(SELECT COUNT(idcandidato) FROM candidato) AS TotalCandidatos,
(SELECT COUNT(*) FROM bem_candidato) AS TotalBemCandidatos,
(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,21 +37,63 @@ 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);
try
{
await candidatoRepository.IncreaseCandidatoPopularity(idcandidato);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error increasing popularity for Candidato ID {idcandidato}");
}
if (result == null)
{
@@ -58,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;
}
@@ -75,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()
@@ -83,16 +142,74 @@ 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()
};
}
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,3 +1,5 @@
using System.IO.Pipes;
namespace OpenCand.Core.Models
{
public class Candidato
@@ -10,20 +12,22 @@ namespace OpenCand.Core.Models
public string Nome { get; set; }
public string Apelido { get; set; }
public DateTime? DataNascimento { get; set; }
public string Email { get; set; }
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; }
}
@@ -35,12 +39,27 @@ namespace OpenCand.Core.Models
public string Nome { 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; }
public string Cargo { get; set; }
public string NrCandidato { get; set; }
public string Resultado { get; set; }
public string Sgpartido { get; set; }
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

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,9 @@
namespace OpenCand.Core.Models
{
public class Partido
{
public string Sigla { get; set; }
public string Nome { get; set; }
public int Numero { 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,7 @@
namespace OpenCand.ETL.Contracts
{
public interface IParserService<CsvObj>
{
Task ParseObject(CsvObj record);
}
}

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

@@ -7,10 +7,11 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
<PackageReference Include="Npgsql" Version="8.0.2" />
<PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" />
</ItemGroup>
<ItemGroup>

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

@@ -1,15 +1,14 @@
using System.Text;
using Microsoft.Extensions.Logging;
using OpenCand.Repository;
namespace OpenCand.Parser.Services
{
public class CsvFixerService
{
private readonly ILogger<CsvParserService> logger;
private readonly ILogger<CsvFixerService> logger;
public CsvFixerService(
ILogger<CsvParserService> logger)
ILogger<CsvFixerService> logger)
{
this.logger = logger;
}
@@ -74,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

@@ -0,0 +1,169 @@
using System.Globalization;
using CsvHelper;
using CsvHelper.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using OpenCand.ETL.Contracts;
namespace OpenCand.Parser.Services
{
public class CsvParserService<CsvObj> : IDisposable
{
private readonly ILogger<CsvParserService<CsvObj>> logger;
private readonly CsvFixerService csvFixerService;
private readonly IParserService<CsvObj> parserService;
private readonly IConfiguration configuration;
private readonly CsvConfiguration parserConfig;
private readonly int MaxDegreeOfParallelism;
// Progress tracking fields
private long processedCount;
private long totalCount;
private string currentTask = string.Empty;
private Timer? progressTimer;
private readonly object progressLock = new object();
public CsvParserService(
ILogger<CsvParserService<CsvObj>> logger,
IParserService<CsvObj> parserService,
CsvFixerService csvFixerService,
IConfiguration configuration)
{
this.logger = logger;
this.csvFixerService = csvFixerService;
this.parserService = parserService;
this.configuration = configuration;
var defaultThreadCount = configuration.GetValue<int>("ParserSettings:DefaultThreads", 25);
if (configuration.GetValue<string>($"ParserSettings:{typeof(CsvObj).Name}Threads") == null)
{
logger.LogInformation($"ParserSettings:{typeof(CsvObj).Name}Threads not found in configuration, using default value of {defaultThreadCount}.");
MaxDegreeOfParallelism = configuration.GetValue<int>("ParserSettings:DefaultThreads", defaultThreadCount);
}
else
{
MaxDegreeOfParallelism = configuration.GetValue<int>($"ParserSettings:{typeof(CsvObj).Name}Threads", defaultThreadCount);
logger.LogInformation($"Using {MaxDegreeOfParallelism} threads for parsing {typeof(CsvObj).Name} based on configuration.");
}
parserConfig = new CsvConfiguration(CultureInfo.InvariantCulture)
{
Delimiter = ";",
HasHeaderRecord = true,
PrepareHeaderForMatch = args => args.Header.ToLower(),
MissingFieldFound = null,
TrimOptions = TrimOptions.Trim,
Encoding = System.Text.Encoding.UTF8
};
}
public async Task ParseFolderAsync(string filePath)
{
logger.LogInformation($"ParseFolderAsync - Starting to parse '{filePath}'");
filePath = csvFixerService.FixCsvFile(filePath);
// Fix the CSV file if necessary
if (string.IsNullOrEmpty(filePath))
{
logger.LogError($"ParseFolderAsync - Failed to fix CSV file at '{filePath}'");
throw new InvalidOperationException($"Failed to fix CSV file at '{filePath}'");
}
try
{
using var reader = new StreamReader(filePath);
using var csv = new CsvReader(reader, parserConfig);
var po = new ParallelOptions
{
MaxDegreeOfParallelism = MaxDegreeOfParallelism
};
//csv.Context.RegisterClassMap<ClassMap<CsvObj>>(); // optional for advanced mapping, not needed
var records = csv.GetRecords<CsvObj>().ToList();
StartProgressTracking($"Parsing {nameof(CsvObj)} - {Path.GetFileName(filePath)}", records.Count);
await Parallel.ForEachAsync(records, po, async (record, ct) =>
{
try
{
await parserService.ParseObject(record);
// Increment progress
IncrementProgress();
}
catch (Exception ex)
{
logger.LogError(ex, $"ParseFolderAsync - Error processing:");
IncrementProgress();
}
});
StopProgressTracking();
logger.LogInformation($"ParseFolderAsync - Finished parsing from {filePath}");
}
catch (Exception ex)
{
logger.LogError(ex, $"ParseFolderAsync - Error parsing file {filePath}");
throw;
}
}
// Progress tracking methods
private void StartProgressTracking(string taskName, long total)
{
lock (progressLock)
{
currentTask = taskName;
processedCount = 0;
totalCount = total;
progressTimer?.Dispose();
progressTimer = new Timer(LogProgress, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
logger.LogInformation("Progress - Task: {Task}, Total: {Total}", currentTask, totalCount);
}
}
private void IncrementProgress()
{
Interlocked.Increment(ref processedCount);
}
private void StopProgressTracking()
{
lock (progressLock)
{
progressTimer?.Dispose();
progressTimer = null;
// Log final progress
var percentage = totalCount > 0 ? (double)processedCount / totalCount * 100 : 0;
logger.LogInformation("Progress - Task: {Task}, Processed: {Processed}, Total: {Total}, Progress: {Percentage:F2}%",
currentTask, processedCount, totalCount, percentage);
}
}
private void LogProgress(object? state)
{
lock (progressLock)
{
if (string.IsNullOrEmpty(currentTask)) return;
var percentage = totalCount > 0 ? (double)processedCount / totalCount * 100 : 0;
logger.LogInformation("Progress - Task: {Task}, Processed: {Processed}, Total: {Total}, Progress: {Percentage:F2}%",
currentTask, processedCount, totalCount, percentage);
}
}
public void Dispose()
{
progressTimer?.Dispose();
}
}
}

View File

@@ -4,30 +4,9 @@ namespace OpenCand.Parser.Models
{
public class BemCandidatoCSV
{
[Name("DT_GERACAO")]
public string DataGeracao { get; set; }
[Name("HH_GERACAO")]
public string HoraGeracao { get; set; }
[Name("ANO_ELEICAO")]
public int AnoEleicao { get; set; }
[Name("CD_TIPO_ELEICAO")]
public int CodigoTipoEleicao { get; set; }
[Name("NM_TIPO_ELEICAO")]
public string NomeTipoEleicao { get; set; }
[Name("CD_ELEICAO")]
public int CodigoEleicao { get; set; }
[Name("DS_ELEICAO")]
public string DescricaoEleicao { get; set; }
[Name("DT_ELEICAO")]
public string DataEleicao { get; set; }
[Name("SG_UF")]
public string SiglaUF { get; set; }
@@ -40,12 +19,9 @@ namespace OpenCand.Parser.Models
[Name("SQ_CANDIDATO")]
public string SequencialCandidato { get; set; }
[Name("NR_ORDEM_BEM_CANDIDATO")]
[Name("NR_ORDEM_BEM_CANDIDATO", "NR_ORDEM_CANDIDATO")]
public int NumeroOrdemBemCandidato { get; set; }
[Name("CD_TIPO_BEM_CANDIDATO")]
public int CodigoTipoBemCandidato { get; set; }
[Name("DS_TIPO_BEM_CANDIDATO")]
public string DescricaoTipoBemCandidato { get; set; }

View File

@@ -5,48 +5,21 @@ namespace OpenCand.Parser.Models
{
public class CandidatoCSV
{
[Name("DT_GERACAO")]
public string DataGeracao { get; set; }
[Name("HH_GERACAO")]
public string HoraGeracao { get; set; }
[Name("ANO_ELEICAO")]
public int AnoEleicao { get; set; }
[Name("CD_TIPO_ELEICAO")]
public int CodigoTipoEleicao { get; set; }
[Name("NM_TIPO_ELEICAO")]
public string NomeTipoEleicao { get; set; }
[Name("NR_TURNO")]
public int NumeroTurno { get; set; }
[Name("CD_ELEICAO")]
public int CodigoEleicao { get; set; }
[Name("DS_ELEICAO")]
public string DescricaoEleicao { get; set; }
[Name("DT_ELEICAO")]
public string DataEleicao { get; set; }
[Name("TP_ABRANGENCIA")]
public string TipoAbrangencia { get; set; }
[Name("NR_TURNO")]
public string Turno { get; set; }
[Name("SG_UF")]
public string SiglaUF { get; set; }
[Name("SG_UE")]
public string SiglaUE { get; set; }
[Name("NM_UE")]
public string NomeUE { get; set; }
[Name("CD_CARGO")]
public int CodigoCargo { get; set; }
[Name("DS_CARGO")]
public string DescricaoCargo { get; set; }
@@ -60,19 +33,13 @@ namespace OpenCand.Parser.Models
public string NomeCandidato { get; set; }
[Name("NM_URNA_CANDIDATO")]
public string NomeUrnaCandidato { get; set; }
[Name("NM_SOCIAL_CANDIDATO")]
public string NomeSocialCandidato { get; set; }
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; }
[Name("SG_UF_NASCIMENTO")]
public string SiglaUFNascimento { get; set; }
public string? Email { get; set; }
[Name("DT_NASCIMENTO")]
public string DataNascimento { get; set; }
@@ -90,6 +57,15 @@ 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; }
[Name("SG_PARTIDO")]
public string SiglaPartido { get; set; }
[Name("NM_PARTIDO")]
public string NomePartido { 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,116 +2,110 @@ 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;
namespace OpenCand.Parser
{
public class ParserManager
{
private readonly CsvParserService csvParserService;
private readonly ILogger<ParserManager> logger;
private readonly CsvSettings csvSettings;
private readonly IConfiguration configuration;
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;
public ParserManager(
CsvParserService csvParserService,
IOptions<CsvSettings> csvSettings,
ILogger<ParserManager> logger,
IConfiguration configuration)
IConfiguration configuration,
CsvParserService<CandidatoCSV> candidatoParserService,
CsvParserService<BemCandidatoCSV> bemCandidatoParserService,
CsvParserService<RedeSocialCSV> redeSocialParserService,
CsvParserService<DespesasCSV> despesaParserService,
CsvParserService<ReceitasCSV> receitaParserService,
DespesaReceitaService despesaReceitaService,
ViewRepository viewRepository)
{
this.csvParserService = csvParserService;
this.logger = logger;
this.csvSettings = csvSettings.Value;
this.configuration = configuration;
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;
if (string.IsNullOrEmpty(BasePath))
{
throw new Exception("ParseFullDataAsync - BasePath is not configured in appsettings.json or CsvSettings.SampleFolder");
}
}
public async Task ParseFullDataAsync()
{
logger.LogInformation("ParseFullDataAsync - Starting parsing");
logger.LogInformation("ParseFullDataAsync - Processing will happen with BasePath: {BasePath}", BasePath);
// Get the base path from either SampleFolder in csvSettings or the BasePath in configuration
var basePath = configuration.GetValue<string>("BasePath");
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);
if (string.IsNullOrEmpty(basePath))
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)
{
if (Directory.Exists(csvDirectory))
{
logger.LogError("ParseFullDataAsync - BasePath is not configured in appsettings.json or CsvSettings.SampleFolder");
return;
foreach (var filePath in Directory.GetFiles(csvDirectory, "*.csv"))
{
// Check if filePath contains "fix_" prefix
if (filePath.Contains("fix_"))
{
logger.LogInformation("ParseFolder - Skipping already fixed file: {FilePath}", filePath);
continue;
}
logger.LogInformation("ParseFolder - Parsing data from {FilePath}", filePath);
await csvParserService.ParseFolderAsync(filePath);
}
}
logger.LogInformation("ParseFullDataAsync - Processing will happen with BasePath: {BasePath}", basePath);
try
else
{
var candidatosDirectory = Path.Combine(basePath, csvSettings.CandidatosFolder);
var bensCandidatosDirectory = Path.Combine(basePath, csvSettings.BensCandidatosFolder);
var redesSociaisDirectory = Path.Combine(basePath, csvSettings.RedesSociaisFolder);
if (Directory.Exists(candidatosDirectory))
{
foreach (var filePath in Directory.GetFiles(candidatosDirectory, "*.csv"))
{
// Check if filePath contains "fix_" prefix
if (filePath.Contains("fix_"))
{
logger.LogInformation("ParseFullDataAsync - Skipping already fixed file: {FilePath}", filePath);
continue;
}
logger.LogInformation("ParseFullDataAsync - Parsing candidatos data from {FilePath}", filePath);
await csvParserService.ParseCandidatosAsync(filePath);
}
}
else
{
logger.LogWarning("ParseFullDataAsync - 'Candidatos' directory not found at {Directory}", candidatosDirectory);
}
if (Directory.Exists(bensCandidatosDirectory))
{
foreach (var filePath in Directory.GetFiles(bensCandidatosDirectory, "*.csv"))
{
// Check if filePath contains "fix_" prefix
if (filePath.Contains("fix_"))
{
logger.LogInformation("ParseFullDataAsync - Skipping already fixed file: {FilePath}", filePath);
continue;
}
logger.LogInformation("ParseFullDataAsync - Parsing bens candidatos data from {FilePath}", filePath);
await csvParserService.ParseBensCandidatosAsync(filePath);
}
}
else
{
logger.LogWarning("ParseFullDataAsync - 'Bens candidatos' directory not found at {Directory}", bensCandidatosDirectory);
}
if (Directory.Exists(redesSociaisDirectory))
{
foreach (var filePath in Directory.GetFiles(redesSociaisDirectory, "*.csv"))
{
// Check if filePath contains "fix_" prefix
if (filePath.Contains("fix_"))
{
logger.LogInformation("ParseFullDataAsync - Skipping already fixed file: {FilePath}", filePath);
continue;
}
logger.LogInformation("ParseFullDataAsync - Parsing redes sociais data from {FilePath}", filePath);
await csvParserService.ParseRedeSocialAsync(filePath);
}
}
else
{
logger.LogWarning("ParseFullDataAsync - 'Redes sociais' directory not found at {Directory}", redesSociaisDirectory);
}
logger.LogInformation("ParseFullDataAsync - Full data parsing completed!");
}
catch (Exception ex)
{
logger.LogError(ex, "ParseFullDataAsync - Error parsing full data set");
throw;
logger.LogWarning("ParseFolder - Directory not found at {Directory}", csvDirectory);
}
}
}

View File

@@ -0,0 +1,53 @@
using Microsoft.Extensions.Logging;
using OpenCand.Core.Models;
using System.Globalization;
using OpenCand.ETL.Contracts;
using OpenCand.Parser.Models;
using OpenCand.Services;
using OpenCand.Parser.Services;
using OpenCand.ETL.Extensions;
namespace OpenCand.ETL.Parser.ParserServices
{
public class BemCandidatoParserService : IParserService<BemCandidatoCSV>
{
private readonly ILogger<BemCandidatoParserService> logger;
private readonly BemCandidatoService bemCandidatoService;
public BemCandidatoParserService(
ILogger<BemCandidatoParserService> logger,
BemCandidatoService bemCandidatoService)
{
this.logger = logger;
this.bemCandidatoService = bemCandidatoService;
}
public async Task ParseObject(BemCandidatoCSV record)
{
// Parse decimal value
decimal? valor = null;
if (!record.ValorBemCandidato.IsNullOrEmpty())
{
string normalizedValue = record.ValorBemCandidato.Replace(".", "").Replace(",", ".");
if (decimal.TryParse(normalizedValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedValue))
{
valor = parsedValue;
}
}
var bemCandidato = new BemCandidato
{
SqCandidato = record.SequencialCandidato,
Ano = record.AnoEleicao,
SiglaUF = record.SiglaUF,
NomeUE = record.NomeUE,
OrdemBem = record.NumeroOrdemBemCandidato,
TipoBem = record.DescricaoTipoBemCandidato,
Descricao = record.DescricaoBemCandidato,
Valor = valor
};
await bemCandidatoService.AddBemCandidatoAsync(bemCandidato);
}
}
}

View File

@@ -0,0 +1,120 @@
using System.Globalization;
using Microsoft.Extensions.Logging;
using OpenCand.Core.Models;
using OpenCand.ETL.Contracts;
using OpenCand.ETL.Extensions;
using OpenCand.Parser.Models;
using OpenCand.Services;
namespace OpenCand.ETL.Parser.ParserServices
{
public class CandidatoParserService : IParserService<CandidatoCSV>
{
private readonly ILogger<CandidatoParserService> logger;
private readonly CandidatoService candidatoService;
public CandidatoParserService(
ILogger<CandidatoParserService> logger,
CandidatoService candidatoService)
{
this.logger = logger;
this.candidatoService = candidatoService;
}
public async Task ParseObject(CandidatoCSV record)
{
if (record.CPFCandidato?.Length <= 3 || record.CPFCandidato.IsNullOrEmpty())
{
record.CPFCandidato = null; // Handle null/empty/whitespace CPF
}
else
{
record.CPFCandidato = record.CPFCandidato.Trim();
}
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 (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.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.Trim(),
SqCandidato = record.SequencialCandidato.Trim(),
Ano = record.AnoEleicao,
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.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()
}
}
};
if (!string.IsNullOrEmpty(record.DataNascimento) &&
record.DataNascimento != "#NULO")
{
if (DateTime.TryParseExact(record.DataNascimento, "dd/MM/yyyy",
CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var dataNascimento))
{
// Convert to UTC DateTime to work with PostgreSQL timestamp with time zone
candidato.DataNascimento = DateTime.SpecifyKind(dataNascimento, DateTimeKind.Utc);
}
}
else
{
candidato.DataNascimento = null; // Handle null/empty/whitespace date
}
await candidatoService.AddCandidatoAsync(candidato);
}
}
}

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

@@ -0,0 +1,37 @@
using Microsoft.Extensions.Logging;
using OpenCand.Core.Models;
using OpenCand.ETL.Contracts;
using OpenCand.Parser.Models;
using OpenCand.Parser.Services;
using OpenCand.Services;
namespace OpenCand.ETL.Parser.ParserServices
{
public class RedeSocialParserService : IParserService<RedeSocialCSV>
{
private readonly ILogger<RedeSocialParserService> logger;
private readonly RedeSocialService redeSocialService;
public RedeSocialParserService(
ILogger<RedeSocialParserService> logger,
RedeSocialService redeSocialService)
{
this.logger = logger;
this.redeSocialService = redeSocialService;
}
public async Task ParseObject(RedeSocialCSV record)
{
var redeSocial = new RedeSocial
{
SqCandidato = record.SequencialCandidato,
Ano = record.DataEleicao,
SiglaUF = record.SiglaUF,
Link = record.Url,
Rede = string.Empty
};
await redeSocialService.AddRedeSocialAsync(redeSocial);
}
}
}

View File

@@ -1,268 +0,0 @@
using System.Globalization;
using CsvHelper;
using CsvHelper.Configuration;
using Microsoft.Extensions.Logging;
using OpenCand.Core.Models;
using OpenCand.ETL.Parser.CsvMappers;
using OpenCand.Parser.CsvMappers;
using OpenCand.Parser.Models;
using OpenCand.Services;
namespace OpenCand.Parser.Services
{
public class CsvParserService
{
private readonly ILogger<CsvParserService> logger;
private readonly CandidatoService candidatoService;
private readonly BemCandidatoService bemCandidatoService;
private readonly RedeSocialService redeSocialService;
private readonly CsvFixerService csvFixerService;
private readonly CsvConfiguration parserConfig;
public CsvParserService(
ILogger<CsvParserService> logger,
CandidatoService candidatoService,
BemCandidatoService bemCandidatoService,
RedeSocialService redeSocialService,
CsvFixerService csvFixerService)
{
this.logger = logger;
this.candidatoService = candidatoService;
this.bemCandidatoService = bemCandidatoService;
this.redeSocialService = redeSocialService;
this.csvFixerService = csvFixerService;
parserConfig = new CsvConfiguration(CultureInfo.InvariantCulture)
{
Delimiter = ";",
HasHeaderRecord = true,
PrepareHeaderForMatch = args => args.Header.ToLower(),
MissingFieldFound = null,
TrimOptions = TrimOptions.Trim,
Encoding = System.Text.Encoding.UTF8
};
}
public async Task ParseCandidatosAsync(string filePath)
{
logger.LogInformation($"ParseCandidatosAsync - Starting to parse 'candidatos' from '{filePath}'");
filePath = csvFixerService.FixCsvFile(filePath);
// Fix the CSV file if necessary
if (string.IsNullOrEmpty(filePath))
{
logger.LogError($"ParseCandidatosAsync - Failed to fix CSV file at '{filePath}'");
throw new InvalidOperationException($"Failed to fix CSV file at '{filePath}'");
}
try
{
using var reader = new StreamReader(filePath);
using var csv = new CsvReader(reader, parserConfig);
var po = new ParallelOptions
{
MaxDegreeOfParallelism = 100
};
csv.Context.RegisterClassMap<CandidatoMap>();
var records = csv.GetRecords<CandidatoCSV>();
await Parallel.ForEachAsync(records, po, async (record, ct) =>
{
try
{
if (string.IsNullOrWhiteSpace(record.CPFCandidato) || record.CPFCandidato.Length <= 3)
{
record.CPFCandidato = null; // Handle null/empty/whitespace CPF
}
if (record.NomeCandidato == "NÃO DIVULGÁVEL" ||
string.IsNullOrEmpty(record.NomeCandidato) ||
record.NomeCandidato == "#NULO")
{
logger.LogCritical($"ParseCandidatosAsync - Candidate with id {record.SequencialCandidato} with invalid name, skipping...");
return; // Skip candidates with invalid name
}
var candidato = new Candidato
{
Cpf = record.CPFCandidato,
SqCandidato = record.SequencialCandidato,
Nome = record.NomeCandidato,
Email = record.Email.Contains("@") ? record.Email : null,
Sexo = record.Genero,
EstadoCivil = record.EstadoCivil,
Escolaridade = record.GrauInstrucao,
Ocupacao = record.Ocupacao,
Eleicoes = new List<CandidatoMapping>()
{
new CandidatoMapping
{
Cpf = record.CPFCandidato,
Nome = record.NomeCandidato,
SqCandidato = record.SequencialCandidato,
Ano = record.AnoEleicao,
TipoEleicao = record.TipoAbrangencia,
NomeUE = record.NomeUE,
SiglaUF = record.SiglaUF,
Cargo = record.DescricaoCargo,
NrCandidato = record.NumeroCandidato,
Resultado = record.SituacaoTurno,
}
}
};
if (!string.IsNullOrEmpty(record.DataNascimento) &&
record.DataNascimento != "#NULO")
{
if (DateTime.TryParseExact(record.DataNascimento, "dd/MM/yyyy",
CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var dataNascimento))
{
// Convert to UTC DateTime to work with PostgreSQL timestamp with time zone
candidato.DataNascimento = DateTime.SpecifyKind(dataNascimento, DateTimeKind.Utc);
}
}
else
{
candidato.DataNascimento = null; // Handle null/empty/whitespace date
}
await candidatoService.AddCandidatoAsync(candidato);
}
catch (Exception ex)
{
logger.LogError(ex, "ParseCandidatosAsync - Error processing candidate with id {CandidatoId}", record.SequencialCandidato);
}
});
logger.LogInformation("ParseCandidatosAsync - Finished parsing candidatos from {FilePath}", filePath);
}
catch (Exception ex)
{
logger.LogError(ex, "ParseCandidatosAsync - Error parsing candidatos file {FilePath}", filePath);
throw;
}
}
public async Task ParseBensCandidatosAsync(string filePath)
{
logger.LogInformation($"ParseBensCandidatosAsync - Starting to parse bens candidatos from '{filePath}'");
filePath = csvFixerService.FixCsvFile(filePath);
// Fix the CSV file if necessary
if (string.IsNullOrEmpty(filePath))
{
logger.LogError($"ParseBensCandidatosAsync - Failed to fix CSV file at '{filePath}'");
throw new InvalidOperationException($"Failed to fix CSV file at '{filePath}'");
}
try
{
using var reader = new StreamReader(filePath);
using var csv = new CsvReader(reader, parserConfig);
csv.Context.RegisterClassMap<BemCandidatoMap>();
var records = csv.GetRecords<BemCandidatoCSV>();
foreach (var record in records)
{
try
{
// Parse decimal value
decimal? valor = null;
if (!string.IsNullOrEmpty(record.ValorBemCandidato))
{
string normalizedValue = record.ValorBemCandidato.Replace(".", "").Replace(",", ".");
if (decimal.TryParse(normalizedValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedValue))
{
valor = parsedValue;
}
}
var bemCandidato = new BemCandidato
{
SqCandidato = record.SequencialCandidato,
Ano = record.AnoEleicao,
SiglaUF = record.SiglaUF,
NomeUE = record.NomeUE,
OrdemBem = record.NumeroOrdemBemCandidato,
TipoBem = record.DescricaoTipoBemCandidato,
Descricao = record.DescricaoBemCandidato,
Valor = valor
};
await bemCandidatoService.AddBemCandidatoAsync(bemCandidato);
}
catch (Exception ex)
{
logger.LogError(ex, "ParseBensCandidatosAsync - Error processing bem candidato with id {CandidatoId} and ordem {OrdemBem}",
record.SequencialCandidato, record.NumeroOrdemBemCandidato);
}
}
logger.LogInformation("ParseBensCandidatosAsync - Finished parsing bens candidatos from {FilePath}", filePath);
}
catch (Exception ex)
{
logger.LogError(ex, "ParseBensCandidatosAsync - Error parsing bens candidatos file {FilePath}", filePath);
throw;
}
}
public async Task ParseRedeSocialAsync(string filePath)
{
logger.LogInformation($"ParseRedeSocialAsync - Starting to parse redes sociais from '{filePath}'");
filePath = csvFixerService.FixCsvFile(filePath);
// Fix the CSV file if necessary
if (string.IsNullOrEmpty(filePath))
{
logger.LogError($"ParseRedeSocialAsync - Failed to fix CSV file at '{filePath}'");
throw new InvalidOperationException($"Failed to fix CSV file at '{filePath}'");
}
try
{
using var reader = new StreamReader(filePath);
using var csv = new CsvReader(reader, parserConfig);
csv.Context.RegisterClassMap<RedeSocialMap>();
var records = csv.GetRecords<RedeSocialCSV>();
foreach (var record in records)
{
try
{
var redeSocial = new RedeSocial
{
SqCandidato = record.SequencialCandidato,
Ano = record.DataEleicao,
SiglaUF = record.SiglaUF,
Link = record.Url,
Rede = string.Empty
};
await redeSocialService.AddRedeSocialAsync(redeSocial);
}
catch (Exception ex)
{
logger.LogError(ex, "ParseRedeSocialAsync - Error processing redes sociais with id {SequencialCandidato} and link {Url}",
record.SequencialCandidato, record.Url);
}
}
logger.LogInformation("ParseRedeSocialAsync - Finished parsing redes sociais from {FilePath}", filePath);
}
catch (Exception ex)
{
logger.LogError(ex, "ParseRedeSocialAsync - Error parsing redes sociais file {FilePath}", filePath);
throw;
}
}
}
}

View File

@@ -3,7 +3,12 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
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;
using OpenCand.Repository;
using OpenCand.Services;
@@ -23,9 +28,6 @@ namespace OpenCand
try
{
logger.LogInformation("Initializing database");
// make a test connection to the database
logger.LogInformation("Starting data parsing");
var parserManager = services.GetRequiredService<ParserManager>();
await parserManager.ParseFullDataAsync();
@@ -54,17 +56,30 @@ namespace OpenCand
{
// Configuration
services.Configure<CsvSettings>(hostContext.Configuration.GetSection("CsvSettings"));
// Services
services.AddTransient<CsvParserService>();
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,28 +16,27 @@ namespace OpenCand.Repository
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO candidato (idcandidato, cpf, nome, datanascimento, email, sexo, estadocivil, escolaridade, ocupacao)
VALUES (@idcandidato, @cpf, @nome, @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;",
localidade = EXCLUDED.localidade,
ultimoano = EXCLUDED.ultimoano
WHERE candidato.ultimoano IS NULL OR EXCLUDED.ultimoano > candidato.ultimoano;",
new
{
idcandidato = candidato.IdCandidato,
cpf = candidato.Cpf,
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
});
}
}
@@ -47,8 +46,9 @@ namespace OpenCand.Repository
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO candidato_mapping (idcandidato, cpf, nome, sqcandidato, ano, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, resultado)
VALUES (@idcandidato, @cpf, @nome, @sqcandidato, @ano, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @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,
@@ -56,37 +56,95 @@ namespace OpenCand.Repository
nome = candidatoMapping.Nome,
sqcandidato = candidatoMapping.SqCandidato,
ano = candidatoMapping.Ano,
turno = candidatoMapping.Turno,
tipoeleicao = candidatoMapping.TipoEleicao,
siglauf = candidatoMapping.SiglaUF,
nomeue = candidatoMapping.NomeUE,
nrcandidato = candidatoMapping.NrCandidato,
cargo = candidatoMapping.Cargo,
sgpartido = candidatoMapping.Partido?.Sigla,
resultado = candidatoMapping.Resultado
});
}
}
public async Task<List<CandidatoMapping>?> GetCandidatoMappingByCpf(string cpf)
public async Task AddCandidatoExtAsync(CandidatoExt candidatoExt)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
var query = @"
SELECT idcandidato, cpf, nome, sqcandidato, ano, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, resultado
FROM candidato_mapping
WHERE cpf = @cpf";
return (await connection.QueryAsync<CandidatoMapping>(query, new { cpf })).AsList();
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<List<CandidatoMapping>?> GetCandidatoMappingByNome(string nome)
public async Task<Candidato?> GetCandidatoByCpf(string cpf)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
var query = @"
SELECT idcandidato, cpf, nome, sqcandidato, ano, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, resultado
SELECT *
FROM candidato
WHERE cpf = @cpf";
return await connection.QueryFirstOrDefaultAsync<Candidato>(query, new { cpf });
}
}
public async Task<Guid?> GetIdCandidatoBySqCandidato(string sqcandidato)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
var query = @"
SELECT idcandidato
FROM candidato_mapping
WHERE nome = @nome";
return (await connection.QueryAsync<CandidatoMapping>(query, new { nome })).AsList();
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))
{
var query = @"
SELECT *
FROM candidato
WHERE nome = @nome AND datanascimento = @datanascimento";
return await connection.QueryFirstOrDefaultAsync<Candidato>(query, new { nome, datanascimento });
}
}
public async Task<CandidatoMapping?> GetCandidatoMappingByDetails(string nome, int ano, string cargo, string siglauf, string nomeue, string nrcandidato, string resultado)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
var query = @"
SELECT *
FROM candidato_mapping
WHERE nome = @nome AND
ano = @ano AND
cargo = @cargo AND
siglauf = @siglauf AND
nomeue = @nomeue AND
nrcandidato = @nrcandidato AND
resultado = @resultado";
return await connection.QueryFirstOrDefaultAsync<CandidatoMapping>(query, new { nome, ano, cargo, siglauf, nomeue, nrcandidato, resultado });
}
}

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,49 @@
using Dapper;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using OpenCand.Core.Models;
using OpenCand.Repository;
namespace OpenCand.ETL.Repository
{
public class PartidoRepository : BaseRepository
{
// Memory cache for partido data
private readonly MemoryCache partidoCache = new MemoryCache(new MemoryCacheOptions());
public PartidoRepository(IConfiguration configuration) : base(configuration)
{
// Initialize the cache with a sliding expiration of 5 minutes
partidoCache = new MemoryCache(new MemoryCacheOptions
{
ExpirationScanFrequency = TimeSpan.FromMinutes(5)
});
}
public async Task AddPartidoAsync(Partido partido)
{
// Check if partido is already cached
if (partidoCache.TryGetValue(partido.Sigla, out Partido? cachedPartido))
{
// If partido is already cached, no need to insert again
return;
}
using (var connection = new Npgsql.NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO partido (sigla, nome, numero)
VALUES (@sigla, @nome, @numero)
ON CONFLICT DO NOTHING;",
new
{
sigla = partido.Sigla,
nome = partido.Nome,
numero = partido.Numero
});
partidoCache.Set(partido.Sigla, partido, TimeSpan.FromMinutes(5));
}
}
}
}

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

@@ -1,4 +1,5 @@
using OpenCand.Core.Models;
using OpenCand.ETL.Repository;
using OpenCand.Repository;
namespace OpenCand.Services
@@ -6,15 +7,17 @@ namespace OpenCand.Services
public class CandidatoService
{
private readonly CandidatoRepository candidatoRepository;
private readonly PartidoRepository partidoRepository;
public CandidatoService(CandidatoRepository candidatoRepository)
public CandidatoService(CandidatoRepository candidatoRepository, PartidoRepository partidoRepository)
{
this.candidatoRepository = candidatoRepository;
this.partidoRepository = partidoRepository;
}
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");
}
@@ -25,60 +28,93 @@ namespace OpenCand.Services
}
var candidatoMapping = candidato.Eleicoes.First();
var candidatoExt = candidato.CandidatoExt.First();
List<CandidatoMapping>? mappings = null;
CandidatoMapping? existingMapping = null;
// Add partido data
if (candidatoMapping.Partido != null)
{
await partidoRepository.AddPartidoAsync(candidatoMapping.Partido);
}
// Check if the candidate already exists in the database
Candidato? existingCandidato;
if (candidato.Cpf == null || candidato.Cpf.Length != 11)
{
mappings = await candidatoRepository.GetCandidatoMappingByNome(candidato.Nome);
existingCandidato = await candidatoRepository.GetCandidatoByNome(candidato.Nome, candidato.DataNascimento.GetValueOrDefault());
}
else
{
mappings = await candidatoRepository.GetCandidatoMappingByCpf(candidato.Cpf);
existingCandidato = await candidatoRepository.GetCandidatoByCpf(candidato.Cpf);
}
// If the candidate already exists, we can update the mappings
if (existingCandidato != null)
{
candidato.IdCandidato = existingCandidato.IdCandidato;
candidato.Cpf = GetNonEmptyString(existingCandidato.Cpf, candidato.Cpf);
candidato.Email = GetNonEmptyString(existingCandidato.Email, candidato.Email);
candidato.Apelido = GetNonEmptyString(existingCandidato.Apelido, candidato.Apelido);
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;
}
// If the candidate does not exist, we create a new one
CandidatoMapping? existingMapping = await candidatoRepository.GetCandidatoMappingByDetails(candidato.Nome,
candidatoMapping.Ano,
candidatoMapping.Cargo,
candidatoMapping.SiglaUF,
candidatoMapping.NomeUE,
candidatoMapping.NrCandidato,
candidatoMapping.Resultado);
// Check if exists
if (mappings != null && mappings.Count > 0)
if (existingMapping != null)
{
existingMapping = mappings.FirstOrDefault(m => m.Ano == candidatoMapping.Ano &&
m.Cargo == candidatoMapping.Cargo &&
m.SiglaUF == candidatoMapping.SiglaUF &&
m.NomeUE == candidatoMapping.NomeUE &&
m.NrCandidato == candidatoMapping.NrCandidato &&
m.Resultado == candidatoMapping.Resultado);
// Already exists one for the current election
if (existingMapping != null)
{
candidato.IdCandidato = existingMapping.IdCandidato;
candidato.Cpf = existingMapping.Cpf;
candidato.IdCandidato = existingMapping.IdCandidato;
candidatoExt.IdCandidato = existingMapping.IdCandidato;
await candidatoRepository.AddCandidatoAsync(candidato);
return;
}
// If exists (but not for the current election), we take the existing idcandidato
// and create a new mapping for the current election
else
{
existingMapping = mappings.First();
candidato.IdCandidato = existingMapping.IdCandidato;
candidato.Cpf = existingMapping.Cpf;
}
}
else
{
// No current mapping, we create a new one
// and create a new mapping for the current election
candidato.IdCandidato = Guid.NewGuid();
candidato.Cpf = GetNonEmptyString(existingMapping.Cpf, candidato.Cpf);
await candidatoRepository.AddCandidatoAsync(candidato);
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
return;
}
// No current mapping, we create a new one and create a new mapping for the current election
candidato.IdCandidato = Guid.NewGuid();
// Set the mapping properties
candidatoMapping.IdCandidato = candidato.IdCandidato;
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)
{
if (!string.IsNullOrWhiteSpace(value1))
{
return value1;
}
else if (!string.IsNullOrWhiteSpace(value2))
{
return value2;
}
return string.Empty;
}
}

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,7 +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": 40,
"DepesasCSVThreads": 50,
"ReceitasCSVThreads": 50
},
"BasePath": "sample"
}

View File

@@ -8,4 +8,91 @@ OpenCand is built using:
* .NET 8 - for parsing initial information from CSV files to the PostgreSQL database using Entity Framework.
* .NET Core 8 - for the API
* PostgreSQL - for the database
* React - for the front-end
* 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 | ⛔ |
| - | - | - |

109
db/db.sql
View File

@@ -1,42 +1,67 @@
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,
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,
sqcandidato TEXT,
sqcandidato VARCHAR(50) NOT NULL,
turno VARCHAR(2) NOT NULL,
ano INT NOT NULL,
tipoeleicao VARCHAR(50),
siglauf VARCHAR(2),
nomeue VARCHAR(100),
cargo VARCHAR(50),
sgpartido VARCHAR(50),
nrcandidato VARCHAR(20),
resultado VARCHAR(50),
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_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,
ano INT NOT NULL,
@@ -50,6 +75,7 @@ ALTER TABLE bem_candidato ADD CONSTRAINT pk_bem_candidato PRIMARY KEY (idcandida
CREATE INDEX idx_bem_candidato_idcandidato ON bem_candidato (idcandidato);
CREATE INDEX idx_bem_candidato_valor ON bem_candidato (valor);
---- Table for storing social media links of candidates
CREATE TABLE rede_social (
idcandidato UUID NOT NULL,
rede VARCHAR(50) NOT NULL,
@@ -59,4 +85,75 @@ CREATE TABLE rede_social (
CONSTRAINT pk_rede_social PRIMARY KEY (idcandidato, rede, siglauf, ano),
CONSTRAINT fk_rede_social_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX idx_rede_social_idcandidato ON rede_social (idcandidato);
CREATE INDEX idx_rede_social_idcandidato ON rede_social (idcandidato);
---- Table for storing party information
CREATE TABLE partido (
sigla VARCHAR(50) NOT NULL PRIMARY KEY,
nome VARCHAR(255) NOT NULL,
numero INT NOT NULL
);
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;