Compare commits
25 Commits
b9908b36b7
...
main
Author | SHA1 | Date | |
---|---|---|---|
579517a1d4 | |||
d4ce3b2577 | |||
a1440baf3d | |||
ddd99ec703 | |||
965b693a19 | |||
ecbf2f07d6 | |||
68d91b8151 | |||
afd6f0298c | |||
fd9e4324dd | |||
f16e1e5e5d | |||
4c72a68481 | |||
0d30afd700 | |||
f5dda37285 | |||
87a98fefb1 | |||
23256245a0 | |||
226d819909 | |||
23b1f0f14e | |||
684a2c0630 | |||
673cda6408 | |||
39faab6483 | |||
5068d348af | |||
93e08a0378 | |||
322e6034bc | |||
e57b3162db | |||
9a107ce9e8 |
@@ -8,6 +8,7 @@ namespace OpenCand.API.Config
|
||||
public const string DefaultPolicy = "DefaultPolicy";
|
||||
public const string CandidatoSearchPolicy = "CandidatoSearchPolicy";
|
||||
public const string CpfRevealPolicy = "CpfRevealPolicy";
|
||||
public const string EstatisticaPolicy = "EstatisticaPolicy";
|
||||
|
||||
public static void ConfigureRateLimiting(this IServiceCollection services)
|
||||
{
|
||||
@@ -26,13 +27,12 @@ namespace OpenCand.API.Config
|
||||
// Default policy: 200 requests per minute with burst of 100
|
||||
options.AddFixedWindowLimiter(policyName: DefaultPolicy, options =>
|
||||
{
|
||||
options.PermitLimit = 200;
|
||||
options.PermitLimit = 400;
|
||||
options.Window = TimeSpan.FromMinutes(1);
|
||||
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||
options.QueueLimit = 100; // Burst capacity
|
||||
});
|
||||
|
||||
// Candidato Search policy: 300 requests per minute with burst of 200
|
||||
options.AddFixedWindowLimiter(policyName: CandidatoSearchPolicy, options =>
|
||||
{
|
||||
options.PermitLimit = 300;
|
||||
@@ -41,23 +41,27 @@ namespace OpenCand.API.Config
|
||||
options.QueueLimit = 200; // Burst capacity
|
||||
});
|
||||
|
||||
// CPF Reveal policy: 15 requests per minute without burst
|
||||
options.AddFixedWindowLimiter(policyName: CpfRevealPolicy, options =>
|
||||
{
|
||||
options.PermitLimit = 15;
|
||||
options.PermitLimit = 20;
|
||||
options.Window = TimeSpan.FromMinutes(1);
|
||||
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||
options.QueueLimit = 0; // No burst
|
||||
options.QueueLimit = 5; // Burst capacity
|
||||
});
|
||||
|
||||
options.OnRejected = async (context, token) =>
|
||||
{
|
||||
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("RateLimitingConfig");
|
||||
var clientIdentifier = GetClientIdentifier(context.HttpContext);
|
||||
logger.LogWarning("Rate limit exceeded for client {ClientIdentifier}", clientIdentifier);
|
||||
|
||||
context.HttpContext.Response.StatusCode = 429;
|
||||
|
||||
var retryAfter = GetRetryAfter(context);
|
||||
if (retryAfter.HasValue)
|
||||
{
|
||||
context.HttpContext.Response.Headers.Add("Retry-After", retryAfter.Value.ToString());
|
||||
context.HttpContext.Response.Headers.Append("Retry-After", retryAfter.Value.ToString());
|
||||
}
|
||||
|
||||
await context.HttpContext.Response.WriteAsync(
|
||||
|
@@ -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
|
||||
{
|
||||
|
||||
|
@@ -1,14 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.VisualBasic;
|
||||
using OpenCand.API.Config;
|
||||
using OpenCand.API.Model;
|
||||
using OpenCand.API.Services;
|
||||
using OpenCand.Core.Models;
|
||||
using OpenCand.Core.Utils;
|
||||
|
||||
namespace OpenCand.API.Controllers
|
||||
{
|
||||
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
|
||||
public class CandidatoController : BaseController
|
||||
{
|
||||
private readonly OpenCandService openCandService;
|
||||
@@ -22,13 +21,34 @@ namespace OpenCand.API.Controllers
|
||||
[EnableRateLimiting(RateLimitingConfig.CandidatoSearchPolicy)]
|
||||
public async Task<CandidatoSearchResult> CandidatoSearch([FromQuery] string q)
|
||||
{
|
||||
return await openCandService.SearchCandidatosAsync(q);
|
||||
if (string.IsNullOrEmpty(q) || q.Length < 3)
|
||||
{
|
||||
throw new ArgumentException("Query parameter 'q' cannot be null/empty.", nameof(q));
|
||||
}
|
||||
|
||||
var result = await openCandService.SearchCandidatosAsync(q);
|
||||
|
||||
result.Candidatos.ForEach(c => c.Cpf = CpfMasking.MaskCpf(c.Cpf));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[HttpGet("random")]
|
||||
public async Task<object> GetRandomCandidatoId()
|
||||
{
|
||||
return new
|
||||
{
|
||||
idCandidato = await openCandService.GetRandomCandidato()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<Candidato> GetCandidatoById([FromRoute] Guid id)
|
||||
{
|
||||
return await openCandService.GetCandidatoAsync(id);
|
||||
var result = await openCandService.GetCandidatoAsync(id);
|
||||
result.Cpf = CpfMasking.MaskCpf(result.Cpf);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[HttpGet("{id}/bens")]
|
||||
@@ -52,5 +72,17 @@ namespace OpenCand.API.Controllers
|
||||
await Task.Delay(randomWait);
|
||||
return await openCandService.GetCandidatoCpfById(id);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/despesas")]
|
||||
public async Task<DespesasResult> GetCandidatoDespesas([FromRoute] Guid id)
|
||||
{
|
||||
return await openCandService.GetDespesasByIdAndYear(id);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/receitas")]
|
||||
public async Task<ReceitaResult> GetCandidatoReceitas([FromRoute] Guid id)
|
||||
{
|
||||
return await openCandService.GetReceitasByIdAndYear(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
35
OpenCand.API/Controllers/EstatisticaController.cs
Normal file
35
OpenCand.API/Controllers/EstatisticaController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -21,5 +21,18 @@ namespace OpenCand.API.Controllers
|
||||
{
|
||||
return await openCandService.GetOpenCandStatsAsync();
|
||||
}
|
||||
|
||||
[HttpGet("data-availability")]
|
||||
public async Task<DataAvailabilityStats> GetDataAvailabilityStats()
|
||||
{
|
||||
return await openCandService.GetDataAvailabilityStatsAsync();
|
||||
}
|
||||
|
||||
[HttpGet("tech")]
|
||||
public async Task<DatabaseTechStats> GetDatabaseTechStats()
|
||||
{
|
||||
return await openCandService.GetDatabaseTechStatsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
10
OpenCand.API/Model/ConfigurationModel.cs
Normal file
10
OpenCand.API/Model/ConfigurationModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
38
OpenCand.API/Model/EstatisticaModels.cs
Normal file
38
OpenCand.API/Model/EstatisticaModels.cs
Normal 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; }
|
||||
}
|
||||
}
|
@@ -21,4 +21,14 @@ namespace OpenCand.API.Model
|
||||
{
|
||||
public string Cpf { get; set; }
|
||||
}
|
||||
|
||||
public class DespesasResult
|
||||
{
|
||||
public List<Despesa> Despesas { get; set; }
|
||||
}
|
||||
|
||||
public class ReceitaResult
|
||||
{
|
||||
public List<Receita> Receitas { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using OpenCand.API.Config;
|
||||
using OpenCand.API.Repository;
|
||||
@@ -35,7 +36,8 @@ namespace OpenCand.API
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(workingDir, "fotos_cand")),
|
||||
RequestPath = "/assets/fotos"
|
||||
}); app.UseHttpsRedirection();
|
||||
});
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
// Use rate limiting middleware
|
||||
app.UseRateLimiter();
|
||||
@@ -56,10 +58,25 @@ namespace OpenCand.API
|
||||
private static void SetupServices(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.Configure<FotosSettings>(builder.Configuration.GetSection("FotosSettings"));
|
||||
builder.Services.AddScoped<OpenCandRepository>();
|
||||
builder.Services.AddScoped<CandidatoRepository>();
|
||||
builder.Services.AddScoped<BemCandidatoRepository>();
|
||||
|
||||
// Configure memory cache with size limit from appsettings
|
||||
var sizeLimitMB = builder.Configuration.GetValue<int>("CacheSettings:SizeLimitMB", 15);
|
||||
builder.Services.AddMemoryCache(options =>
|
||||
{
|
||||
options.SizeLimit = sizeLimitMB * 1024L * 1024L; // Convert MB to bytes
|
||||
});
|
||||
|
||||
builder.Services.AddScoped(provider => new OpenCandRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||
builder.Services.AddScoped(provider => new CandidatoRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||
builder.Services.AddScoped(provider => new BemCandidatoRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||
builder.Services.AddScoped(provider => new DespesaReceitaRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||
builder.Services.AddScoped(provider => new EstatisticaRepository(provider.GetRequiredService<IConfiguration>(), provider.GetService<IMemoryCache>()));
|
||||
|
||||
builder.Services.AddScoped<OpenCandService>();
|
||||
builder.Services.AddScoped<EstatisticaService>();
|
||||
|
||||
// Add cache preload background service
|
||||
builder.Services.AddHostedService<CachePreloadService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Npgsql;
|
||||
using OpenCand.Core.Models;
|
||||
|
||||
@@ -6,40 +7,35 @@ namespace OpenCand.Repository
|
||||
{
|
||||
public class CandidatoRepository : BaseRepository
|
||||
{
|
||||
public CandidatoRepository(IConfiguration configuration) : base(configuration)
|
||||
public CandidatoRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<List<Candidato>> SearchCandidatosAsync(string query)
|
||||
public async Task<List<Candidato>?> SearchCandidatosAsync(string query)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
string cacheKey = GenerateCacheKey("Search", query);
|
||||
|
||||
return await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
using var connection = new NpgsqlConnection(ConnectionString);
|
||||
return (await connection.QueryAsync<Candidato>(@"
|
||||
SELECT *,
|
||||
CASE
|
||||
WHEN lower(apelido) = lower(@query) THEN 0 -- apelido Exact match (case-insensitive)
|
||||
WHEN lower(apelido) LIKE lower(@query) || '%' THEN 1 -- apelido Starts with match (case-insensitive)
|
||||
WHEN lower(apelido) LIKE '%' || lower(@query) THEN 2 -- apelido Contains anywhere match (case-insensitive)
|
||||
WHEN lower(nome) = lower(@query) THEN 0 -- nome Exact match (case-insensitive)
|
||||
WHEN lower(nome) LIKE lower(@query) || '%' THEN 1 -- nome Starts with match (case-insensitive)
|
||||
WHEN lower(nome) LIKE '%' || lower(@query) THEN 2 -- nome Contains anywhere match (case-insensitive)
|
||||
WHEN cpf = @query THEN 0 -- cpf Exact match for CPF
|
||||
WHEN cpf LIKE @query || '%' THEN 1 -- cpf Starts with match for CPF
|
||||
WHEN cpf LIKE '%' || @query THEN 2 -- cpf Contains anywhere match for CPF
|
||||
ELSE 3
|
||||
END AS name_rank
|
||||
FROM candidato
|
||||
WHERE apelido ILIKE '%' || @query || '%' OR
|
||||
nome ILIKE '%' || @query || '%' OR
|
||||
cpf ILIKE '%' || @query || '%'
|
||||
ORDER BY name_rank,
|
||||
length(nome) ASC
|
||||
LIMIT 10;",
|
||||
new { query })).AsList();
|
||||
}
|
||||
SELECT c.*,
|
||||
GREATEST(similarity(c.apelido, @q), similarity(c.nome, @q)) AS sim
|
||||
FROM candidato c
|
||||
WHERE c.apelido % @q
|
||||
OR c.nome % @q
|
||||
ORDER BY c.popularidade DESC, sim DESC, length(c.nome) ASC
|
||||
LIMIT 10;
|
||||
", new { q = query })).AsList();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public async Task<Candidato?> GetCandidatoAsync(Guid idcandidato)
|
||||
{
|
||||
string cacheKey = GenerateCacheKey("Candidato", idcandidato);
|
||||
|
||||
return await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
@@ -48,6 +44,7 @@ namespace OpenCand.Repository
|
||||
WHERE idcandidato = @idcandidato;",
|
||||
new { idcandidato });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<string?> GetCandidatoCpfAsync(Guid idcandidato)
|
||||
@@ -93,5 +90,37 @@ namespace OpenCand.Repository
|
||||
return (await connection.QueryAsync<RedeSocial>(query, new { idcandidato })).AsList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<CandidatoExt>?> GetCandidatoExtById(Guid idcandidato)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
var query = @"
|
||||
SELECT * FROM candidato_ext
|
||||
WHERE idcandidato = @idcandidato";
|
||||
return (await connection.QueryAsync<CandidatoExt>(query, new { idcandidato })).AsList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task IncreaseCandidatoPopularity(Guid idcandidato)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(ConnectionString);
|
||||
await connection.ExecuteAsync(@"
|
||||
UPDATE candidato
|
||||
SET popularidade = popularidade + 1
|
||||
WHERE idcandidato = @idcandidato;
|
||||
", new { idcandidato });
|
||||
}
|
||||
|
||||
public async Task<Guid?> GetRandomCandidatoIdAsync()
|
||||
{
|
||||
using var connection = new NpgsqlConnection(ConnectionString);
|
||||
return await connection.QueryFirstOrDefaultAsync<Guid?>(@"
|
||||
SELECT idcandidato
|
||||
FROM candidato
|
||||
TABLESAMPLE SYSTEM (0.01)
|
||||
LIMIT 1;
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
43
OpenCand.API/Repository/DespesaReceitaRepository.cs
Normal file
43
OpenCand.API/Repository/DespesaReceitaRepository.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
124
OpenCand.API/Repository/EstatisticaRepository.cs
Normal file
124
OpenCand.API/Repository/EstatisticaRepository.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Npgsql;
|
||||
using OpenCand.Core.Models;
|
||||
using OpenCand.Repository;
|
||||
@@ -7,11 +8,18 @@ namespace OpenCand.API.Repository
|
||||
{
|
||||
public class OpenCandRepository : BaseRepository
|
||||
{
|
||||
public OpenCandRepository(IConfiguration configuration) : base(configuration)
|
||||
private readonly IConfiguration configuration;
|
||||
|
||||
public OpenCandRepository(IConfiguration configuration, IMemoryCache? cache = null) : base(configuration, cache)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<OpenCandStats> GetOpenCandStatsAsync()
|
||||
{
|
||||
string cacheKey = GenerateCacheKey("OpenCandStats");
|
||||
|
||||
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
@@ -22,8 +30,114 @@ namespace OpenCand.API.Repository
|
||||
(SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos,
|
||||
(SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais,
|
||||
(SELECT COUNT(DISTINCT ano) FROM candidato_mapping) AS TotalEleicoes;");
|
||||
|
||||
return stats ?? new OpenCandStats();
|
||||
}
|
||||
});
|
||||
|
||||
return result ?? new OpenCandStats();
|
||||
}
|
||||
|
||||
public async Task<DataAvailabilityStats> GetDataAvailabilityAsync()
|
||||
{
|
||||
string cacheKey = GenerateCacheKey("DataAvailabilityStats");
|
||||
|
||||
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
var stats = new DataAvailabilityStats();
|
||||
|
||||
// Get years for each data type separately
|
||||
var candidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM candidato_mapping ORDER BY ano DESC");
|
||||
var bemCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM bem_candidato ORDER BY ano DESC");
|
||||
var despesaCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM despesas_candidato ORDER BY ano DESC");
|
||||
var receitaCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM receitas_candidato ORDER BY ano DESC");
|
||||
var redeSocialCandidatosYears = await connection.QueryAsync<int>("SELECT DISTINCT ano FROM rede_social ORDER BY ano DESC");
|
||||
|
||||
stats.Candidatos = candidatosYears.ToList();
|
||||
stats.BemCandidatos = bemCandidatosYears.ToList();
|
||||
stats.DespesaCandidatos = despesaCandidatosYears.ToList();
|
||||
stats.ReceitaCandidatos = receitaCandidatosYears.ToList();
|
||||
stats.RedeSocialCandidatos = redeSocialCandidatosYears.ToList();
|
||||
|
||||
// Get all folders from appsetting `FotosSettings__BasePath`
|
||||
string basePath = configuration["FotosSettings:Path"] ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(basePath))
|
||||
throw new InvalidOperationException("Base path for photos is not configured.");
|
||||
|
||||
var directories = Directory.GetDirectories(basePath);
|
||||
if (directories.Any())
|
||||
stats.FotosCandidatos = directories
|
||||
.Select(dir => dir.Split(Path.DirectorySeparatorChar).Last().Split("_")[1].Replace("cand", ""))
|
||||
.Select(ano => Convert.ToInt32(ano))
|
||||
.Distinct()
|
||||
.OrderByDescending(ano => ano)
|
||||
.ToList();
|
||||
|
||||
return stats;
|
||||
}
|
||||
});
|
||||
|
||||
return result ?? new DataAvailabilityStats();
|
||||
}
|
||||
|
||||
public async Task<DatabaseTechStats> GetDatabaseTechStatsAsync()
|
||||
{
|
||||
string cacheKey = GenerateCacheKey("DatabaseTechStats");
|
||||
|
||||
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
var stats = new DatabaseTechStats();
|
||||
|
||||
|
||||
// Get table stats using reltuples for entries
|
||||
var tableStats = await connection.QueryAsync<TableStats>(@"
|
||||
SELECT
|
||||
pt.schemaname||'.'||pt.tablename as Name,
|
||||
pg_total_relation_size(pt.schemaname||'.'||pt.tablename) as TotalSize,
|
||||
COALESCE(c.reltuples,0)::bigint as Entries
|
||||
FROM pg_tables pt
|
||||
JOIN pg_class c ON c.relname = pt.tablename AND c.relkind = 'r'
|
||||
WHERE pt.schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(pt.schemaname||'.'||pt.tablename) DESC;");
|
||||
|
||||
|
||||
var tableStatsList = tableStats.ToList();
|
||||
stats.Tables = tableStatsList;
|
||||
stats.TotalSize = tableStatsList.Sum(t => t.TotalSize);
|
||||
stats.TotalEntries = tableStatsList.Sum(t => t.Entries);
|
||||
|
||||
// Get materialized view stats using reltuples for entries
|
||||
var materializedViewStats = await connection.QueryAsync<TableStats>(@"
|
||||
SELECT
|
||||
pmv.schemaname||'.'||pmv.matviewname as Name,
|
||||
pg_total_relation_size(pmv.schemaname||'.'||pmv.matviewname) as TotalSize,
|
||||
COALESCE(c.reltuples,0)::bigint as Entries
|
||||
FROM pg_matviews pmv
|
||||
JOIN pg_class c ON c.relname = pmv.matviewname AND c.relkind = 'm'
|
||||
WHERE pmv.schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(pmv.schemaname||'.'||pmv.matviewname) DESC;");
|
||||
|
||||
stats.MaterializedViews = materializedViewStats.ToList();
|
||||
|
||||
// Get index stats
|
||||
var indexStats = await connection.QueryFirstOrDefaultAsync<IndexStats>(@"
|
||||
SELECT
|
||||
COUNT(*) as Amount,
|
||||
SUM(pg_relation_size(indexrelid)) as Size
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE schemaname = 'public';");
|
||||
|
||||
stats.Indexes = indexStats ?? new IndexStats();
|
||||
|
||||
return stats;
|
||||
}
|
||||
});
|
||||
|
||||
return result ?? new DatabaseTechStats();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
98
OpenCand.API/Services/CachePreloadService.cs
Normal file
98
OpenCand.API/Services/CachePreloadService.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
176
OpenCand.API/Services/EstatisticaService.cs
Normal file
176
OpenCand.API/Services/EstatisticaService.cs
Normal 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.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ namespace OpenCand.API.Services
|
||||
private readonly OpenCandRepository openCandRepository;
|
||||
private readonly CandidatoRepository candidatoRepository;
|
||||
private readonly BemCandidatoRepository bemCandidatoRepository;
|
||||
private readonly DespesaReceitaRepository despesaReceitaRepository;
|
||||
private readonly IConfiguration configuration;
|
||||
private readonly FotosSettings fotoSettings;
|
||||
private readonly ILogger<OpenCandService> logger;
|
||||
@@ -20,6 +21,7 @@ namespace OpenCand.API.Services
|
||||
OpenCandRepository openCandRepository,
|
||||
CandidatoRepository candidatoRepository,
|
||||
BemCandidatoRepository bemCandidatoRepository,
|
||||
DespesaReceitaRepository despesaReceitaRepository,
|
||||
IOptions<FotosSettings> fotoSettings,
|
||||
IConfiguration configuration,
|
||||
ILogger<OpenCandService> logger)
|
||||
@@ -27,6 +29,7 @@ namespace OpenCand.API.Services
|
||||
this.openCandRepository = openCandRepository;
|
||||
this.candidatoRepository = candidatoRepository;
|
||||
this.bemCandidatoRepository = bemCandidatoRepository;
|
||||
this.despesaReceitaRepository = despesaReceitaRepository;
|
||||
this.fotoSettings = fotoSettings.Value;
|
||||
this.configuration = configuration;
|
||||
this.logger = logger;
|
||||
@@ -34,25 +37,62 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<OpenCandStats> GetOpenCandStatsAsync()
|
||||
{
|
||||
logger.LogInformation("Getting OpenCand stats");
|
||||
|
||||
return await openCandRepository.GetOpenCandStatsAsync();
|
||||
}
|
||||
|
||||
public async Task<DataAvailabilityStats> GetDataAvailabilityStatsAsync()
|
||||
{
|
||||
logger.LogInformation("Getting data availability stats");
|
||||
|
||||
var stats = await openCandRepository.GetDataAvailabilityAsync();
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
public async Task<DatabaseTechStats> GetDatabaseTechStatsAsync()
|
||||
{
|
||||
logger.LogInformation("Getting database tech stats");
|
||||
|
||||
var stats = await openCandRepository.GetDatabaseTechStatsAsync();
|
||||
|
||||
stats.Tables = stats.Tables.OrderBy(t => t.Name).ToList();
|
||||
stats.MaterializedViews = stats.MaterializedViews.OrderBy(mv => mv.Name).ToList();
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
public async Task<CandidatoSearchResult> SearchCandidatosAsync(string query)
|
||||
{
|
||||
logger.LogInformation($"Searching candidatos with query: {query}");
|
||||
|
||||
return new CandidatoSearchResult()
|
||||
{
|
||||
Candidatos = await candidatoRepository.SearchCandidatosAsync(query)
|
||||
Candidatos = await candidatoRepository.SearchCandidatosAsync(query) ?? new List<Candidato>()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Guid?> GetRandomCandidato()
|
||||
{
|
||||
logger.LogInformation("Getting random candidato");
|
||||
|
||||
return await candidatoRepository.GetRandomCandidatoIdAsync();
|
||||
}
|
||||
|
||||
public async Task<Candidato> GetCandidatoAsync(Guid idcandidato)
|
||||
{
|
||||
var result = await candidatoRepository.GetCandidatoAsync(idcandidato);
|
||||
var eleicoes = await candidatoRepository.GetCandidatoMappingById(idcandidato);
|
||||
var candidatoExt = await candidatoRepository.GetCandidatoExtById(idcandidato);
|
||||
|
||||
foreach (var eleicao in eleicoes)
|
||||
try
|
||||
{
|
||||
eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido);
|
||||
await candidatoRepository.IncreaseCandidatoPopularity(idcandidato);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"Error increasing popularity for Candidato ID {idcandidato}");
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
@@ -63,11 +103,23 @@ namespace OpenCand.API.Services
|
||||
{
|
||||
throw new KeyNotFoundException($"CandidatoMapping for ID {idcandidato} not found.");
|
||||
}
|
||||
if (candidatoExt == null)
|
||||
{
|
||||
throw new KeyNotFoundException($"CandidatoExt for ID {idcandidato} not found.");
|
||||
}
|
||||
|
||||
foreach (var eleicao in eleicoes)
|
||||
{
|
||||
eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido);
|
||||
}
|
||||
|
||||
var lastEleicao = eleicoes.OrderByDescending(e => e.Ano).First();
|
||||
|
||||
result.FotoUrl = $"{fotoSettings.ApiBasePath}/foto_cand{lastEleicao.Ano}_{lastEleicao.SiglaUF}_div/F{lastEleicao.SiglaUF}{lastEleicao.SqCandidato}_div.jpg";
|
||||
result.Eleicoes = eleicoes.OrderByDescending(e => e.Ano).ToList();
|
||||
result.CandidatoExt = candidatoExt.OrderByDescending(ce => ce.Ano).ToList();
|
||||
|
||||
logger.LogDebug($"Found Candidato: {result.Nome}, ID: {result.IdCandidato}, CPF: {result.Cpf}, FotoUrl: {result.FotoUrl}");
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -80,6 +132,8 @@ namespace OpenCand.API.Services
|
||||
result = new List<BemCandidato>();
|
||||
}
|
||||
|
||||
logger.LogInformation($"Found {result.Count} bens for Candidato ID {idcandidato}");
|
||||
|
||||
return new BemCandidatoResult()
|
||||
{
|
||||
Bens = result.OrderByDescending(r => r.TipoBem).ThenByDescending(r => r.Valor).ToList()
|
||||
@@ -88,12 +142,16 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<RedeSocialResult> GetCandidatoRedeSocialById(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting redes sociais for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await candidatoRepository.GetCandidatoRedeSocialById(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
result = new List<RedeSocial>();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found {result.Count} redes sociais for Candidato ID {idcandidato}");
|
||||
|
||||
return new RedeSocialResult()
|
||||
{
|
||||
RedesSociais = result.OrderByDescending(r => r.Ano).ToList()
|
||||
@@ -102,16 +160,56 @@ namespace OpenCand.API.Services
|
||||
|
||||
public async Task<CpfRevealResult> GetCandidatoCpfById(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting CPF for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await candidatoRepository.GetCandidatoCpfAsync(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
return new CpfRevealResult();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found CPF {result} for Candidato ID {idcandidato}");
|
||||
|
||||
return new CpfRevealResult()
|
||||
{
|
||||
Cpf = result
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DespesasResult> GetDespesasByIdAndYear(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting despesas for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await despesaReceitaRepository.GetDespesasByCandidatoIdYearAsync(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
return new DespesasResult();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found {result.Count} despesas for Candidato ID {idcandidato}");
|
||||
|
||||
return new DespesasResult()
|
||||
{
|
||||
Despesas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ReceitaResult> GetReceitasByIdAndYear(Guid idcandidato)
|
||||
{
|
||||
logger.LogInformation($"Getting receitas for Candidato ID {idcandidato}");
|
||||
|
||||
var result = await despesaReceitaRepository.GetReceitasByCandidatoIdYearAsync(idcandidato);
|
||||
if (result == null)
|
||||
{
|
||||
return new ReceitaResult();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Found {result.Count} receitas for Candidato ID {idcandidato}");
|
||||
|
||||
return new ReceitaResult()
|
||||
{
|
||||
Receitas = result.OrderByDescending(d => d.Ano).ThenByDescending(d => d.Valor).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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": "*"
|
||||
}
|
||||
|
@@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
cd ./fotos_cand
|
||||
COUNT=0
|
||||
shopt -s nocaseglob
|
||||
|
||||
# Loop through all folders
|
||||
for dir in */; do
|
||||
# Change into the directory
|
||||
cd "$dir" || continue
|
||||
|
||||
# Loop over every “.jpeg” (or “.JPEG”):
|
||||
for f in *.jpeg; do
|
||||
# “${f%.[jJ][pP][eE][gG]}” strips off the .jpeg/.JPEG suffix
|
||||
base="${f%.[jJ][pP][eE][gG]}"
|
||||
newfile="${base}.jpg"
|
||||
|
||||
# If there’s already a .jpg with the same “base,” decide what to do:
|
||||
if [ -e "$newfile" ]; then
|
||||
echo "Skipping $f → $newfile (target exists)"
|
||||
# you could `rm "$f"` or move it to a backup folder here if you prefer
|
||||
else
|
||||
mv -v "$f" "$newfile"
|
||||
fi
|
||||
done
|
||||
|
||||
# Change back to the parent directory
|
||||
cd ..
|
||||
done
|
||||
|
||||
shopt -u nocaseglob
|
||||
|
||||
# Print a message indicating completion
|
||||
echo "Normalization complete. Processed $COUNT files."
|
@@ -20,14 +20,14 @@ namespace OpenCand.Core.Models
|
||||
|
||||
public string Sexo { get; set; }
|
||||
|
||||
public string EstadoCivil { get; set; }
|
||||
public string Localidade { get; set; }
|
||||
|
||||
public string Escolaridade { get; set; }
|
||||
|
||||
public string Ocupacao { get; set; }
|
||||
public int Ultimoano { get; set; }
|
||||
|
||||
public List<CandidatoMapping> Eleicoes { get; set; }
|
||||
|
||||
public List<CandidatoExt> CandidatoExt { get; set; }
|
||||
|
||||
// API ONLY
|
||||
public string FotoUrl { get; set; }
|
||||
}
|
||||
@@ -37,9 +37,9 @@ namespace OpenCand.Core.Models
|
||||
public Guid IdCandidato { get; set; }
|
||||
public string Cpf { get; set; }
|
||||
public string Nome { get; set; }
|
||||
public string Apelido { get; set; }
|
||||
public string SqCandidato { get; set; }
|
||||
public int Ano { get; set; }
|
||||
public string Turno { get; set; }
|
||||
public string TipoEleicao { get; set; }
|
||||
public string SiglaUF { get; set; }
|
||||
public string NomeUE { get; set; }
|
||||
@@ -51,6 +51,17 @@ namespace OpenCand.Core.Models
|
||||
public Partido? Partido { get; set; } // Nullable to allow for candidates without a party
|
||||
}
|
||||
|
||||
public class CandidatoExt
|
||||
{
|
||||
public Guid IdCandidato { get; set; }
|
||||
public int Ano { get; set; }
|
||||
public string Apelido { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string EstadoCivil { get; set; }
|
||||
public string Escolaridade { get; set; }
|
||||
public string Ocupacao { get; set; }
|
||||
}
|
||||
|
||||
public class RedeSocial
|
||||
{
|
||||
public Guid IdCandidato { get; set; }
|
||||
|
25
OpenCand.Core/Models/Despesa.cs
Normal file
25
OpenCand.Core/Models/Despesa.cs
Normal 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; }
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
26
OpenCand.Core/Models/Receita.cs
Normal file
26
OpenCand.Core/Models/Receita.cs
Normal 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; }
|
||||
}
|
||||
}
|
21
OpenCand.Core/Utils/CpfMasking.cs
Normal file
21
OpenCand.Core/Utils/CpfMasking.cs
Normal 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)}";
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
15
OpenCand.ETL/Extensions/StringExtensions.cs
Normal file
15
OpenCand.ETL/Extensions/StringExtensions.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -73,8 +73,10 @@ namespace OpenCand.Parser.Services
|
||||
{
|
||||
if (columns.Length > headerCount)
|
||||
{
|
||||
logger.LogCritical($"FixCsvFile - Line {i + 1} has {columns.Length} columns, expected {headerCount}. Halting process.");
|
||||
return string.Empty; // Critical error, cannot fix this line => needs manual intervention
|
||||
// logger.LogCritical($"FixCsvFile - Line {i + 1} has {columns.Length} columns, expected {headerCount}. Halting process.");
|
||||
// return string.Empty; // Critical error, cannot fix this line => needs manual intervention
|
||||
// consider as normal line
|
||||
break;
|
||||
}
|
||||
|
||||
logger.LogWarning($"FixCsvFile - Line {i + 1} has {columns.Length} columns, expected {headerCount}. Attempting to fix [i = {lineJump}]...");
|
||||
|
@@ -11,6 +11,9 @@ namespace OpenCand.Parser.Models
|
||||
[Name("TP_ABRANGENCIA")]
|
||||
public string TipoAbrangencia { get; set; }
|
||||
|
||||
[Name("NR_TURNO")]
|
||||
public string Turno { get; set; }
|
||||
|
||||
[Name("SG_UF")]
|
||||
public string SiglaUF { get; set; }
|
||||
|
||||
@@ -33,10 +36,10 @@ namespace OpenCand.Parser.Models
|
||||
public string Apelido { get; set; }
|
||||
|
||||
[Name("NR_CPF_CANDIDATO")]
|
||||
public string CPFCandidato { get; set; }
|
||||
public string? CPFCandidato { get; set; }
|
||||
|
||||
[Name("DS_EMAIL", "NM_EMAIL")]
|
||||
public string Email { get; set; }
|
||||
public string? Email { get; set; }
|
||||
|
||||
[Name("DT_NASCIMENTO")]
|
||||
public string DataNascimento { get; set; }
|
||||
@@ -54,7 +57,7 @@ namespace OpenCand.Parser.Models
|
||||
public string GrauInstrucao { get; set; }
|
||||
|
||||
[Name("DS_SIT_TOT_TURNO")]
|
||||
public string SituacaoTurno { get; set; }
|
||||
public string? SituacaoTurno { get; set; }
|
||||
|
||||
[Name("NR_PARTIDO")]
|
||||
public int NumeroPartido { get; set; }
|
||||
|
97
OpenCand.ETL/Parser/Models/DespesasCSV.cs
Normal file
97
OpenCand.ETL/Parser/Models/DespesasCSV.cs
Normal 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; }
|
||||
}
|
||||
}
|
94
OpenCand.ETL/Parser/Models/ReceitasCSV.cs
Normal file
94
OpenCand.ETL/Parser/Models/ReceitasCSV.cs
Normal 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; }
|
||||
}
|
||||
}
|
@@ -2,6 +2,8 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenCand.Config;
|
||||
using OpenCand.ETL.Repository;
|
||||
using OpenCand.ETL.Services;
|
||||
using OpenCand.Parser.Models;
|
||||
using OpenCand.Parser.Services;
|
||||
|
||||
@@ -15,6 +17,12 @@ namespace OpenCand.Parser
|
||||
private readonly CsvParserService<CandidatoCSV> candidatoParserService;
|
||||
private readonly CsvParserService<BemCandidatoCSV> bemCandidatoParserService;
|
||||
private readonly CsvParserService<RedeSocialCSV> redeSocialParserService;
|
||||
private readonly CsvParserService<DespesasCSV> despesaParserService;
|
||||
private readonly CsvParserService<ReceitasCSV> receitaParserService;
|
||||
|
||||
private readonly DespesaReceitaService despesaReceitaService;
|
||||
|
||||
private readonly ViewRepository viewRepository;
|
||||
|
||||
private readonly string BasePath;
|
||||
|
||||
@@ -24,7 +32,11 @@ namespace OpenCand.Parser
|
||||
IConfiguration configuration,
|
||||
CsvParserService<CandidatoCSV> candidatoParserService,
|
||||
CsvParserService<BemCandidatoCSV> bemCandidatoParserService,
|
||||
CsvParserService<RedeSocialCSV> redeSocialParserService)
|
||||
CsvParserService<RedeSocialCSV> redeSocialParserService,
|
||||
CsvParserService<DespesasCSV> despesaParserService,
|
||||
CsvParserService<ReceitasCSV> receitaParserService,
|
||||
DespesaReceitaService despesaReceitaService,
|
||||
ViewRepository viewRepository)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.csvSettings = csvSettings.Value;
|
||||
@@ -32,6 +44,10 @@ namespace OpenCand.Parser
|
||||
this.candidatoParserService = candidatoParserService;
|
||||
this.bemCandidatoParserService = bemCandidatoParserService;
|
||||
this.redeSocialParserService = redeSocialParserService;
|
||||
this.despesaParserService = despesaParserService;
|
||||
this.receitaParserService = receitaParserService;
|
||||
this.despesaReceitaService = despesaReceitaService;
|
||||
this.viewRepository = viewRepository;
|
||||
|
||||
// Get the base path from either SampleFolder in csvSettings or the BasePath in configuration
|
||||
BasePath = configuration.GetValue<string>("BasePath") ?? string.Empty;
|
||||
@@ -49,12 +65,25 @@ namespace OpenCand.Parser
|
||||
var candidatosDirectory = Path.Combine(BasePath, csvSettings.CandidatosFolder);
|
||||
var bensCandidatosDirectory = Path.Combine(BasePath, csvSettings.BensCandidatosFolder);
|
||||
var redesSociaisDirectory = Path.Combine(BasePath, csvSettings.RedesSociaisFolder);
|
||||
var despesasDirectory = Path.Combine(BasePath, csvSettings.DespesaCandidatoFolder);
|
||||
var receitasDirectory = Path.Combine(BasePath, csvSettings.ReceitaCandidatoFolder);
|
||||
|
||||
await ParseFolder(candidatosDirectory, candidatoParserService);
|
||||
await ParseFolder(bensCandidatosDirectory, bemCandidatoParserService);
|
||||
await ParseFolder(redesSociaisDirectory, redeSocialParserService);
|
||||
|
||||
await despesaReceitaService.DeleteDespesaAsync();
|
||||
await ParseFolder(despesasDirectory, despesaParserService);
|
||||
await despesaReceitaService.DeleteReceitaAsync();
|
||||
await ParseFolder(receitasDirectory, receitaParserService);
|
||||
|
||||
logger.LogInformation("ParseFullDataAsync - Full data parsing completed!");
|
||||
|
||||
logger.LogInformation("ParseFullDataAsync - Will refresh materialized views and re-run the analyzes.");
|
||||
|
||||
await viewRepository.RefreshMaterializedViews();
|
||||
|
||||
logger.LogInformation("ParseFullDataAsync - Materialized views refreshed successfully!");
|
||||
}
|
||||
|
||||
private async Task ParseFolder<CsvObj>(string csvDirectory, CsvParserService<CsvObj> csvParserService)
|
||||
|
@@ -5,6 +5,7 @@ using OpenCand.ETL.Contracts;
|
||||
using OpenCand.Parser.Models;
|
||||
using OpenCand.Services;
|
||||
using OpenCand.Parser.Services;
|
||||
using OpenCand.ETL.Extensions;
|
||||
|
||||
namespace OpenCand.ETL.Parser.ParserServices
|
||||
{
|
||||
@@ -25,7 +26,7 @@ namespace OpenCand.ETL.Parser.ParserServices
|
||||
{
|
||||
// Parse decimal value
|
||||
decimal? valor = null;
|
||||
if (!string.IsNullOrEmpty(record.ValorBemCandidato))
|
||||
if (!record.ValorBemCandidato.IsNullOrEmpty())
|
||||
{
|
||||
string normalizedValue = record.ValorBemCandidato.Replace(".", "").Replace(",", ".");
|
||||
if (decimal.TryParse(normalizedValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedValue))
|
||||
|
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenCand.Core.Models;
|
||||
using OpenCand.ETL.Contracts;
|
||||
using OpenCand.ETL.Extensions;
|
||||
using OpenCand.Parser.Models;
|
||||
using OpenCand.Services;
|
||||
|
||||
@@ -22,60 +23,79 @@ namespace OpenCand.ETL.Parser.ParserServices
|
||||
|
||||
public async Task ParseObject(CandidatoCSV record)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(record.CPFCandidato) || record.CPFCandidato.Length <= 3)
|
||||
if (record.CPFCandidato?.Length <= 3 || record.CPFCandidato.IsNullOrEmpty())
|
||||
{
|
||||
record.CPFCandidato = null; // Handle null/empty/whitespace CPF
|
||||
}
|
||||
else
|
||||
{
|
||||
record.CPFCandidato = record.CPFCandidato.Trim();
|
||||
}
|
||||
|
||||
if (record.NomeCandidato == "NÃO DIVULGÁVEL" ||
|
||||
string.IsNullOrEmpty(record.NomeCandidato) ||
|
||||
record.NomeCandidato == "#NULO")
|
||||
if (record.Apelido.IsNullOrEmpty())
|
||||
{
|
||||
record.Apelido = null;
|
||||
}
|
||||
|
||||
if (record.NomeCandidato.IsNullOrEmpty())
|
||||
{
|
||||
logger.LogCritical($"ParseCandidatosAsync - Candidate with id {record.SequencialCandidato} with invalid name, skipping...");
|
||||
return; // Skip candidates with invalid name
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(record.Apelido) ||
|
||||
record.Apelido.Contains("#NUL") ||
|
||||
record.Apelido.Contains("NULO#") ||
|
||||
record.Apelido.Contains("#NE"))
|
||||
if (record.Apelido.IsNullOrEmpty())
|
||||
{
|
||||
record.Apelido = null;
|
||||
}
|
||||
|
||||
if (record.SituacaoTurno.IsNullOrEmpty())
|
||||
{
|
||||
record.SituacaoTurno = null;
|
||||
}
|
||||
|
||||
var candidato = new Candidato
|
||||
{
|
||||
Cpf = record.CPFCandidato,
|
||||
SqCandidato = record.SequencialCandidato,
|
||||
Nome = record.NomeCandidato,
|
||||
Apelido = record.Apelido,
|
||||
Email = record.Email.Contains("@") ? record.Email : null,
|
||||
Sexo = record.Genero,
|
||||
EstadoCivil = record.EstadoCivil,
|
||||
Escolaridade = record.GrauInstrucao,
|
||||
Ocupacao = record.Ocupacao,
|
||||
Nome = record.NomeCandidato.Trim(),
|
||||
Apelido = record.Apelido?.Trim(),
|
||||
Sexo = record.Genero.Trim(),
|
||||
Localidade = record.NomeUE.Trim(),
|
||||
Ultimoano = record.AnoEleicao,
|
||||
Eleicoes = new List<CandidatoMapping>()
|
||||
{
|
||||
new CandidatoMapping
|
||||
{
|
||||
Cpf = record.CPFCandidato,
|
||||
Nome = record.NomeCandidato,
|
||||
Apelido = record.Apelido,
|
||||
SqCandidato = record.SequencialCandidato,
|
||||
Nome = record.NomeCandidato.Trim(),
|
||||
SqCandidato = record.SequencialCandidato.Trim(),
|
||||
Ano = record.AnoEleicao,
|
||||
TipoEleicao = record.TipoAbrangencia,
|
||||
NomeUE = record.NomeUE,
|
||||
SiglaUF = record.SiglaUF,
|
||||
Cargo = record.DescricaoCargo,
|
||||
NrCandidato = record.NumeroCandidato,
|
||||
Resultado = record.SituacaoTurno,
|
||||
Turno = record.Turno.Trim(),
|
||||
TipoEleicao = record.TipoAbrangencia.Trim(),
|
||||
NomeUE = record.NomeUE.Trim(),
|
||||
SiglaUF = record.SiglaUF.Trim(),
|
||||
Cargo = record.DescricaoCargo.Trim(),
|
||||
NrCandidato = record.NumeroCandidato.Trim(),
|
||||
Resultado = record.SituacaoTurno?.Trim() ?? "-",
|
||||
Partido = new Partido
|
||||
{
|
||||
Sigla = record.SiglaPartido,
|
||||
Nome = record.NomePartido,
|
||||
Sigla = record.SiglaPartido.Trim(),
|
||||
Nome = record.NomePartido.Trim(),
|
||||
Numero = record.NumeroPartido,
|
||||
}
|
||||
}
|
||||
},
|
||||
CandidatoExt = new List<CandidatoExt>()
|
||||
{
|
||||
new CandidatoExt
|
||||
{
|
||||
Apelido = record.Apelido?.Trim(),
|
||||
EstadoCivil = record.EstadoCivil.Trim(),
|
||||
Escolaridade = record.GrauInstrucao.Trim(),
|
||||
Ocupacao = record.Ocupacao.Trim(),
|
||||
Ano = record.AnoEleicao,
|
||||
Email = record.Email.IsNullOrEmpty() ? null : record.Email.Trim()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
88
OpenCand.ETL/Parser/ParserServices/DespesaParserService.cs
Normal file
88
OpenCand.ETL/Parser/ParserServices/DespesaParserService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
84
OpenCand.ETL/Parser/ParserServices/ReceitaParserService.cs
Normal file
84
OpenCand.ETL/Parser/ParserServices/ReceitaParserService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ using OpenCand.Config;
|
||||
using OpenCand.ETL.Contracts;
|
||||
using OpenCand.ETL.Parser.ParserServices;
|
||||
using OpenCand.ETL.Repository;
|
||||
using OpenCand.ETL.Services;
|
||||
using OpenCand.Parser;
|
||||
using OpenCand.Parser.Models;
|
||||
using OpenCand.Parser.Services;
|
||||
@@ -60,18 +61,25 @@ namespace OpenCand
|
||||
services.AddTransient<IParserService<CandidatoCSV>, CandidatoParserService>();
|
||||
services.AddTransient<IParserService<BemCandidatoCSV>, BemCandidatoParserService>();
|
||||
services.AddTransient<IParserService<RedeSocialCSV>, RedeSocialParserService>();
|
||||
services.AddTransient<IParserService<DespesasCSV>, DespesaParserService>();
|
||||
services.AddTransient<IParserService<ReceitasCSV>, ReceitaParserService>();
|
||||
services.AddTransient<CsvParserService<CandidatoCSV>>();
|
||||
services.AddTransient<CsvParserService<BemCandidatoCSV>>();
|
||||
services.AddTransient<CsvParserService<RedeSocialCSV>>();
|
||||
services.AddTransient<CsvParserService<DespesasCSV>>();
|
||||
services.AddTransient<CsvParserService<ReceitasCSV>>();
|
||||
services.AddTransient<ParserManager>();
|
||||
services.AddTransient<DespesaReceitaService>();
|
||||
services.AddTransient<CandidatoService>();
|
||||
services.AddTransient<BemCandidatoService>();
|
||||
services.AddTransient<RedeSocialService>();
|
||||
services.AddTransient<DespesaReceitaRepository>();
|
||||
services.AddTransient<CandidatoRepository>();
|
||||
services.AddTransient<BemCandidatoRepository>();
|
||||
services.AddTransient<RedeSocialRepository>();
|
||||
services.AddTransient<PartidoRepository>();
|
||||
services.AddTransient<CsvFixerService>();
|
||||
services.AddTransient<ViewRepository>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -16,18 +16,17 @@ namespace OpenCand.Repository
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
await connection.ExecuteAsync(@"
|
||||
INSERT INTO candidato (idcandidato, cpf, nome, apelido, datanascimento, email, sexo, estadocivil, escolaridade, ocupacao)
|
||||
VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @email, @sexo, @estadocivil, @escolaridade, @ocupacao)
|
||||
INSERT INTO candidato (idcandidato, cpf, nome, apelido, datanascimento, sexo, ultimoano, localidade)
|
||||
VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @sexo, @ultimoano, @localidade)
|
||||
ON CONFLICT (idcandidato) DO UPDATE SET
|
||||
cpf = EXCLUDED.cpf,
|
||||
nome = EXCLUDED.nome,
|
||||
apelido = EXCLUDED.apelido,
|
||||
datanascimento = EXCLUDED.datanascimento,
|
||||
email = EXCLUDED.email,
|
||||
sexo = EXCLUDED.sexo,
|
||||
estadocivil = EXCLUDED.estadocivil,
|
||||
escolaridade = EXCLUDED.escolaridade,
|
||||
ocupacao = EXCLUDED.ocupacao,
|
||||
apelido = EXCLUDED.apelido;",
|
||||
localidade = EXCLUDED.localidade,
|
||||
ultimoano = EXCLUDED.ultimoano
|
||||
WHERE candidato.ultimoano IS NULL OR EXCLUDED.ultimoano > candidato.ultimoano;",
|
||||
new
|
||||
{
|
||||
idcandidato = candidato.IdCandidato,
|
||||
@@ -35,11 +34,9 @@ namespace OpenCand.Repository
|
||||
nome = candidato.Nome,
|
||||
apelido = candidato.Apelido,
|
||||
datanascimento = candidato.DataNascimento,
|
||||
email = candidato.Email,
|
||||
sexo = candidato.Sexo,
|
||||
estadocivil = candidato.EstadoCivil,
|
||||
escolaridade = candidato.Escolaridade,
|
||||
ocupacao = candidato.Ocupacao
|
||||
localidade = candidato.Localidade,
|
||||
ultimoano = candidato.Ultimoano
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,17 +46,17 @@ namespace OpenCand.Repository
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
await connection.ExecuteAsync(@"
|
||||
INSERT INTO candidato_mapping (idcandidato, cpf, nome, apelido, sqcandidato, ano, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado)
|
||||
VALUES (@idcandidato, @cpf, @nome, @apelido, @sqcandidato, @ano, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado)
|
||||
INSERT INTO candidato_mapping (idcandidato, cpf, nome, sqcandidato, ano, turno, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado)
|
||||
VALUES (@idcandidato, @cpf, @nome, @sqcandidato, @ano, @turno, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado)
|
||||
ON CONFLICT DO NOTHING;",
|
||||
new
|
||||
{
|
||||
idcandidato = candidatoMapping.IdCandidato,
|
||||
cpf = candidatoMapping.Cpf,
|
||||
nome = candidatoMapping.Nome,
|
||||
apelido = candidatoMapping.Apelido,
|
||||
sqcandidato = candidatoMapping.SqCandidato,
|
||||
ano = candidatoMapping.Ano,
|
||||
turno = candidatoMapping.Turno,
|
||||
tipoeleicao = candidatoMapping.TipoEleicao,
|
||||
siglauf = candidatoMapping.SiglaUF,
|
||||
nomeue = candidatoMapping.NomeUE,
|
||||
@@ -71,6 +68,32 @@ namespace OpenCand.Repository
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddCandidatoExtAsync(CandidatoExt candidatoExt)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
await connection.ExecuteAsync(@"
|
||||
INSERT INTO candidato_ext (idcandidato, ano, apelido, email, estadocivil, escolaridade, ocupacao)
|
||||
VALUES (@idcandidato, @ano, @apelido, @email, @estadocivil, @escolaridade, @ocupacao)
|
||||
ON CONFLICT (idcandidato, ano) DO UPDATE SET
|
||||
apelido = EXCLUDED.apelido,
|
||||
email = EXCLUDED.email,
|
||||
estadocivil = EXCLUDED.estadocivil,
|
||||
escolaridade = EXCLUDED.escolaridade,
|
||||
ocupacao = EXCLUDED.ocupacao;",
|
||||
new
|
||||
{
|
||||
idcandidato = candidatoExt.IdCandidato,
|
||||
ano = candidatoExt.Ano,
|
||||
apelido = candidatoExt.Apelido,
|
||||
email = candidatoExt.Email,
|
||||
estadocivil = candidatoExt.EstadoCivil,
|
||||
escolaridade = candidatoExt.Escolaridade,
|
||||
ocupacao = candidatoExt.Ocupacao
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Candidato?> GetCandidatoByCpf(string cpf)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
@@ -83,6 +106,18 @@ namespace OpenCand.Repository
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Guid?> GetIdCandidatoBySqCandidato(string sqcandidato)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
{
|
||||
var query = @"
|
||||
SELECT idcandidato
|
||||
FROM candidato_mapping
|
||||
WHERE sqcandidato = @sqcandidato";
|
||||
return await connection.QueryFirstOrDefaultAsync<Guid>(query, new { sqcandidato });
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Candidato?> GetCandidatoByNome(string nome, DateTime datanascimento)
|
||||
{
|
||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||
|
155
OpenCand.ETL/Repository/DespesaReceitaRepository.cs
Normal file
155
OpenCand.ETL/Repository/DespesaReceitaRepository.cs
Normal 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;");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
OpenCand.ETL/Repository/ViewRepository.cs
Normal file
36
OpenCand.ETL/Repository/ViewRepository.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,7 +17,7 @@ namespace OpenCand.Services
|
||||
|
||||
public async Task AddCandidatoAsync(Candidato candidato)
|
||||
{
|
||||
if (candidato == null)
|
||||
if (candidato == null || candidato.CandidatoExt == null || candidato.Eleicoes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(candidato), "Candidato cannot be null");
|
||||
}
|
||||
@@ -28,6 +28,7 @@ namespace OpenCand.Services
|
||||
}
|
||||
|
||||
var candidatoMapping = candidato.Eleicoes.First();
|
||||
var candidatoExt = candidato.CandidatoExt.First();
|
||||
|
||||
// Add partido data
|
||||
if (candidatoMapping.Partido != null)
|
||||
@@ -52,18 +53,18 @@ namespace OpenCand.Services
|
||||
candidato.IdCandidato = existingCandidato.IdCandidato;
|
||||
candidato.Cpf = GetNonEmptyString(existingCandidato.Cpf, candidato.Cpf);
|
||||
candidato.Email = GetNonEmptyString(existingCandidato.Email, candidato.Email);
|
||||
candidato.EstadoCivil = GetNonEmptyString(existingCandidato.EstadoCivil, candidato.EstadoCivil);
|
||||
candidato.Apelido = GetNonEmptyString(existingCandidato.Apelido, candidato.Apelido);
|
||||
candidato.Escolaridade = GetNonEmptyString(existingCandidato.Escolaridade, candidato.Escolaridade);
|
||||
candidato.Ocupacao = GetNonEmptyString(existingCandidato.Ocupacao, candidato.Ocupacao);
|
||||
candidato.Sexo = GetNonEmptyString(existingCandidato.Sexo, candidato.Sexo);
|
||||
|
||||
candidatoMapping.IdCandidato = candidato.IdCandidato;
|
||||
candidatoMapping.Cpf = GetNonEmptyString(candidato.Cpf, candidatoMapping.Cpf);
|
||||
|
||||
candidatoExt.IdCandidato = candidato.IdCandidato;
|
||||
|
||||
// Update the entries for the existing candidate
|
||||
await candidatoRepository.AddCandidatoAsync(candidato);
|
||||
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
|
||||
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,10 +80,12 @@ namespace OpenCand.Services
|
||||
if (existingMapping != null)
|
||||
{
|
||||
candidato.IdCandidato = existingMapping.IdCandidato;
|
||||
candidato.Cpf = GetNonEmptyString(existingMapping.Cpf, candidato.Cpf);
|
||||
candidato.Apelido = GetNonEmptyString(existingMapping.Apelido, candidato.Apelido);
|
||||
await candidatoRepository.AddCandidatoAsync(candidato);
|
||||
candidatoExt.IdCandidato = existingMapping.IdCandidato;
|
||||
|
||||
candidato.Cpf = GetNonEmptyString(existingMapping.Cpf, candidato.Cpf);
|
||||
|
||||
await candidatoRepository.AddCandidatoAsync(candidato);
|
||||
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,8 +97,11 @@ namespace OpenCand.Services
|
||||
candidatoMapping.Cpf = candidato.Cpf;
|
||||
candidatoMapping.Nome = candidato.Nome;
|
||||
|
||||
candidatoExt.IdCandidato = candidato.IdCandidato;
|
||||
|
||||
await candidatoRepository.AddCandidatoAsync(candidato);
|
||||
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
|
||||
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
|
||||
}
|
||||
|
||||
public string GetNonEmptyString(string? value1, string? value2)
|
||||
|
64
OpenCand.ETL/Services/DespesaReceitaService.cs
Normal file
64
OpenCand.ETL/Services/DespesaReceitaService.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -12,11 +12,15 @@
|
||||
"CsvSettings": {
|
||||
"CandidatosFolder": "data/consulta_cand",
|
||||
"BensCandidatosFolder": "data/bem_candidato",
|
||||
"RedesSociaisFolder": "data/rede_social"
|
||||
"RedesSociaisFolder": "data/rede_social",
|
||||
"DespesaCandidatoFolder": "data/despesas_candidato",
|
||||
"ReceitaCandidatoFolder": "data/receitas_candidato"
|
||||
},
|
||||
"ParserSettings": {
|
||||
"DefaultThreads": 40,
|
||||
"CandidatoCSVThreads": 5
|
||||
"CandidatoCSVThreads": 40,
|
||||
"DepesasCSVThreads": 50,
|
||||
"ReceitasCSVThreads": 50
|
||||
},
|
||||
"BasePath": "sample"
|
||||
}
|
||||
|
87
README.md
87
README.md
@@ -9,3 +9,90 @@ OpenCand is built using:
|
||||
* .NET Core 8 - for the API
|
||||
* PostgreSQL - for the database
|
||||
* React - for the front-end
|
||||
|
||||
## Disponibilidade de dados (Prod)
|
||||
|
||||
* ✅ = Disponível
|
||||
* ❌ = Não disponível
|
||||
* ⛔ = Sem dados
|
||||
|
||||
| Nome do dado | Ano | Disponível? |
|
||||
|-------------------|------|-------------|
|
||||
| Candidatos | 2024 | ✅ |
|
||||
| Bem Candidato | 2024 | ✅ |
|
||||
| Despesas/Receitas | 2024 | ✅ |
|
||||
| Rede Social | 2024 | ✅ |
|
||||
| Fotos | 2024 | ✅ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2022 | ✅ |
|
||||
| Bem Candidato | 2022 | ✅ |
|
||||
| Despesas/Receitas | 2022 | ✅ |
|
||||
| Rede Social | 2022 | ✅ |
|
||||
| Fotos | 2022 | ✅ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2020 | ✅ |
|
||||
| Bem Candidato | 2020 | ✅ |
|
||||
| Despesas/Receitas | 2020 | ✅ |
|
||||
| Rede Social | 2020 | ✅ |
|
||||
| Fotos | 2020 | ❌ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2018 | ✅ |
|
||||
| Bem Candidato | 2018 | ✅ |
|
||||
| Despesas/Receitas | 2018 | ❌ |
|
||||
| Rede Social | 2018 | ❌ |
|
||||
| Fotos | 2018 | ❌ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2016 | ✅ |
|
||||
| Bem Candidato | 2016 | ✅ |
|
||||
| Despesas/Receitas | 2016 | ❌ |
|
||||
| Rede Social | 2016 | ❌ |
|
||||
| Fotos | 2016 | ❌ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2014 | ✅ |
|
||||
| Bem Candidato | 2014 | ✅ |
|
||||
| Despesas/Receitas | 2014 | ❌ |
|
||||
| Rede Social | 2014 | ❌ |
|
||||
| Fotos | 2014 | ❌ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2012 | ✅ |
|
||||
| Bem Candidato | 2012 | ✅ |
|
||||
| Despesas/Receitas | 2012 | ❌ |
|
||||
| Rede Social | 2012 | ❌ |
|
||||
| Fotos | 2012 | ❌ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2010 | ✅ |
|
||||
| Bem Candidato | 2010 | ✅ |
|
||||
| Despesas/Receitas | 2010 | ❌ |
|
||||
| Rede Social | 2010 | ❌ |
|
||||
| Fotos | 2010 | ❌ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2008 | ✅ |
|
||||
| Bem Candidato | 2008 | ✅ |
|
||||
| Despesas/Receitas | 2008 | ❌ |
|
||||
| Rede Social | 2008 | ❌ |
|
||||
| Fotos | 2008 | ❌ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2006 | ✅ |
|
||||
| Bem Candidato | 2006 | ✅ |
|
||||
| Despesas/Receitas | 2006 | ❌ |
|
||||
| Rede Social | 2006 | ❌ |
|
||||
| Fotos | 2006 | ❌ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2004 | ✅ |
|
||||
| Bem Candidato | 2004 | ❌ |
|
||||
| Despesas/Receitas | 2004 | ❌ |
|
||||
| Rede Social | 2004 | ❌ |
|
||||
| Fotos | 2004 | ❌ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2002 | ✅ |
|
||||
| Bem Candidato | 2002 | ❌ |
|
||||
| Despesas/Receitas | 2002 | ❌ |
|
||||
| Rede Social | 2002 | ❌ |
|
||||
| Fotos | 2002 | ❌ |
|
||||
| - | - | - |
|
||||
| Candidatos | 2000 | ✅ |
|
||||
| Bem Candidato | 2000 | ⛔ |
|
||||
| Despesas/Receitas | 2000 | ⛔ |
|
||||
| Rede Social | 2000 | ⛔ |
|
||||
| Fotos | 2000 | ⛔ |
|
||||
| - | - | - |
|
||||
|
96
db/db.sql
96
db/db.sql
@@ -1,31 +1,36 @@
|
||||
DROP TABLE IF EXISTS bem_candidato CASCADE;
|
||||
DROP TABLE IF EXISTS candidato_mapping CASCADE;
|
||||
DROP TABLE IF EXISTS candidato_ext CASCADE;
|
||||
DROP TABLE IF EXISTS rede_social CASCADE;
|
||||
DROP TABLE IF EXISTS candidato CASCADE;
|
||||
DROP TABLE IF EXISTS partido CASCADE;
|
||||
DROP TABLE IF EXISTS despesas_candidato CASCADE;
|
||||
DROP TABLE IF EXISTS receitas_candidato CASCADE;
|
||||
|
||||
CREATE TABLE candidato (
|
||||
idcandidato UUID NOT NULL PRIMARY KEY,
|
||||
cpf VARCHAR(11),
|
||||
nome VARCHAR(255) NOT NULL,
|
||||
apelido VARCHAR(255),
|
||||
datanascimento TIMESTAMPTZ,
|
||||
email TEXT,
|
||||
sexo CHAR(15),
|
||||
estadocivil VARCHAR(50),
|
||||
escolaridade VARCHAR(50),
|
||||
ocupacao VARCHAR(150)
|
||||
apelido VARCHAR(255),
|
||||
localidade VARCHAR(100),
|
||||
ultimoano INT,
|
||||
popularidade BIGINT DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_candidato_nome ON candidato (nome);
|
||||
CREATE INDEX idx_candidato_apelido ON candidato (apelido);
|
||||
CREATE INDEX idx_candidato_datanascimento ON candidato (datanascimento);
|
||||
CREATE INDEX idx_candidato_nomenascimento ON candidato (nome, datanascimento);
|
||||
CREATE INDEX idx_candidato_cpf ON candidato (cpf);
|
||||
|
||||
-- Each candidato (idcandidato, cpf, nome) will be mapped to a (sqcandidato, ano, tipo_eleicao, sg_uf, cargo, resultado)
|
||||
CREATE TABLE candidato_mapping (
|
||||
idcandidato UUID NOT NULL,
|
||||
cpf VARCHAR(11),
|
||||
nome VARCHAR(255) NOT NULL,
|
||||
apelido VARCHAR(255),
|
||||
sqcandidato TEXT,
|
||||
sqcandidato VARCHAR(50) NOT NULL,
|
||||
turno VARCHAR(2) NOT NULL,
|
||||
ano INT NOT NULL,
|
||||
tipoeleicao VARCHAR(50),
|
||||
siglauf VARCHAR(2),
|
||||
@@ -37,12 +42,25 @@ CREATE TABLE candidato_mapping (
|
||||
CONSTRAINT pk_candidato_mapping PRIMARY KEY (idcandidato, ano, siglauf, nomeue, cargo, nrcandidato, resultado),
|
||||
CONSTRAINT fk_candidato_mapping_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_candidato_mapping_idcandidato ON candidato_mapping (idcandidato);
|
||||
CREATE INDEX idx_candidato_mapping_cpf ON candidato_mapping (cpf);
|
||||
CREATE INDEX idx_candidato_mapping_nome ON candidato_mapping (nome);
|
||||
CREATE INDEX idx_candidato_mapping_apelido ON candidato_mapping (apelido);
|
||||
CREATE INDEX idx_candidato_mapping_ano ON candidato_mapping (ano);
|
||||
CREATE INDEX idx_candidato_mapping_sqcandidato ON candidato_mapping (sqcandidato);
|
||||
|
||||
CREATE TABLE candidato_ext (
|
||||
idcandidato UUID NOT NULL,
|
||||
ano INT NOT NULL,
|
||||
apelido VARCHAR(255),
|
||||
email TEXT,
|
||||
estadocivil VARCHAR(50),
|
||||
escolaridade VARCHAR(50),
|
||||
ocupacao TEXT,
|
||||
CONSTRAINT pk_candidato_ext PRIMARY KEY (idcandidato, ano)
|
||||
);
|
||||
CREATE INDEX idx_candidato_ext_idcandidato ON candidato_ext (idcandidato);
|
||||
CREATE INDEX idx_candidato_ext_ano ON candidato_ext (ano);
|
||||
|
||||
---- Table for storing assets of candidates
|
||||
CREATE TABLE bem_candidato (
|
||||
idcandidato UUID NOT NULL,
|
||||
@@ -77,3 +95,65 @@ CREATE TABLE partido (
|
||||
);
|
||||
CREATE INDEX idx_partido_nome ON partido (nome);
|
||||
CREATE INDEX idx_partido_numero ON partido (numero);
|
||||
|
||||
---- Tables for storing despesas e receitas of candidacies
|
||||
CREATE TABLE despesas_candidato (
|
||||
iddespesa UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
idcandidato UUID NOT NULL,
|
||||
ano INT NOT NULL,
|
||||
turno VARCHAR(2) NOT NULL,
|
||||
sqcandidato VARCHAR(50) NOT NULL,
|
||||
sgpartido VARCHAR(50) NOT NULL,
|
||||
tipofornecedor VARCHAR(150),
|
||||
cnpjfornecedor VARCHAR(14),
|
||||
cpffornecedor VARCHAR(11),
|
||||
nomefornecedor VARCHAR(255),
|
||||
nomefornecedorrfb VARCHAR(255),
|
||||
municipiofornecedor VARCHAR(100),
|
||||
tipodocumento VARCHAR(50),
|
||||
datadespesa TIMESTAMPTZ,
|
||||
descricao TEXT,
|
||||
origemdespesa TEXT,
|
||||
valor NUMERIC(20, 2),
|
||||
CONSTRAINT pk_despesas_candidato PRIMARY KEY (iddespesa),
|
||||
CONSTRAINT fk_despesas_candidato_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_despesas_candidato_idcandidato ON despesas_candidato (idcandidato);
|
||||
CREATE INDEX idx_despesas_candidato_ano ON despesas_candidato (ano);
|
||||
CREATE INDEX idx_despesas_candidato_sqcandidato ON despesas_candidato (sqcandidato);
|
||||
CREATE INDEX idx_despesas_candidato_sgpartido ON despesas_candidato (sgpartido);
|
||||
|
||||
CREATE TABLE receitas_candidato (
|
||||
idreceita UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
idcandidato UUID NOT NULL,
|
||||
ano INT NOT NULL,
|
||||
turno VARCHAR(2) NOT NULL,
|
||||
sqcandidato VARCHAR(50) NOT NULL,
|
||||
sgpartido VARCHAR(50) NOT NULL,
|
||||
fontereceita VARCHAR(150),
|
||||
origemreceita VARCHAR(250),
|
||||
naturezareceita VARCHAR(250),
|
||||
especiereceita VARCHAR(250),
|
||||
cnpjdoador VARCHAR(14),
|
||||
cpfdoador VARCHAR(11),
|
||||
nomedoador VARCHAR(255),
|
||||
nomedoadorrfb VARCHAR(255),
|
||||
municipiodoador VARCHAR(100),
|
||||
datareceita TIMESTAMPTZ,
|
||||
descricao TEXT,
|
||||
valor NUMERIC(20, 2),
|
||||
CONSTRAINT pk_receitas_candidato PRIMARY KEY (idreceita),
|
||||
CONSTRAINT fk_receitas_candidato_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_receitas_candidato_idcandidato ON receitas_candidato (idcandidato);
|
||||
CREATE INDEX idx_receitas_candidato_ano ON receitas_candidato (ano);
|
||||
CREATE INDEX idx_receitas_candidato_sqcandidato ON receitas_candidato (sqcandidato);
|
||||
CREATE INDEX idx_receitas_candidato_sgpartido ON receitas_candidato (sgpartido);
|
||||
|
||||
|
||||
|
||||
-- Search function
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE INDEX idx_candidato_nome_trgm ON candidato USING GIN (nome gin_trgm_ops);
|
||||
CREATE INDEX idx_candidato_apelido_trgm ON candidato USING GIN (apelido gin_trgm_ops);
|
||||
CREATE INDEX idx_candidato_cpf_trgm ON candidato USING GIN (cpf gin_trgm_ops);
|
92
db/mv.sql
Normal file
92
db/mv.sql
Normal 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;
|
Reference in New Issue
Block a user