Compare commits
27 Commits
2660826a3f
...
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 | |||
b9908b36b7 | |||
a7732dfccf |
95
OpenCand.API/Config/RateLimitingConfig.cs
Normal file
95
OpenCand.API/Config/RateLimitingConfig.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using System.Threading.RateLimiting;
|
||||||
|
|
||||||
|
namespace OpenCand.API.Config
|
||||||
|
{
|
||||||
|
public static class RateLimitingConfig
|
||||||
|
{
|
||||||
|
public const string DefaultPolicy = "DefaultPolicy";
|
||||||
|
public const string CandidatoSearchPolicy = "CandidatoSearchPolicy";
|
||||||
|
public const string CpfRevealPolicy = "CpfRevealPolicy";
|
||||||
|
public const string EstatisticaPolicy = "EstatisticaPolicy";
|
||||||
|
|
||||||
|
public static void ConfigureRateLimiting(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddRateLimiter(options =>
|
||||||
|
{
|
||||||
|
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
|
||||||
|
RateLimitPartition.GetFixedWindowLimiter(
|
||||||
|
partitionKey: GetClientIdentifier(httpContext),
|
||||||
|
factory: partition => new FixedWindowRateLimiterOptions
|
||||||
|
{
|
||||||
|
AutoReplenishment = true,
|
||||||
|
PermitLimit = 2000, // Global limit per minute
|
||||||
|
Window = TimeSpan.FromMinutes(1)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Default policy: 200 requests per minute with burst of 100
|
||||||
|
options.AddFixedWindowLimiter(policyName: DefaultPolicy, options =>
|
||||||
|
{
|
||||||
|
options.PermitLimit = 400;
|
||||||
|
options.Window = TimeSpan.FromMinutes(1);
|
||||||
|
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
|
options.QueueLimit = 100; // Burst capacity
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddFixedWindowLimiter(policyName: CandidatoSearchPolicy, options =>
|
||||||
|
{
|
||||||
|
options.PermitLimit = 300;
|
||||||
|
options.Window = TimeSpan.FromMinutes(1);
|
||||||
|
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
|
options.QueueLimit = 200; // Burst capacity
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddFixedWindowLimiter(policyName: CpfRevealPolicy, options =>
|
||||||
|
{
|
||||||
|
options.PermitLimit = 20;
|
||||||
|
options.Window = TimeSpan.FromMinutes(1);
|
||||||
|
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
|
options.QueueLimit = 5; // Burst capacity
|
||||||
|
});
|
||||||
|
|
||||||
|
options.OnRejected = async (context, token) =>
|
||||||
|
{
|
||||||
|
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
|
||||||
|
var logger = loggerFactory.CreateLogger("RateLimitingConfig");
|
||||||
|
var clientIdentifier = GetClientIdentifier(context.HttpContext);
|
||||||
|
logger.LogWarning("Rate limit exceeded for client {ClientIdentifier}", clientIdentifier);
|
||||||
|
|
||||||
|
context.HttpContext.Response.StatusCode = 429;
|
||||||
|
|
||||||
|
var retryAfter = GetRetryAfter(context);
|
||||||
|
if (retryAfter.HasValue)
|
||||||
|
{
|
||||||
|
context.HttpContext.Response.Headers.Append("Retry-After", retryAfter.Value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.HttpContext.Response.WriteAsync(
|
||||||
|
"Rate limit exceeded. Please try again later.", cancellationToken: token);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetClientIdentifier(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
// Get client IP address for partitioning
|
||||||
|
var clientIp = httpContext.Connection.RemoteIpAddress?.ToString()
|
||||||
|
?? httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault()
|
||||||
|
?? "unknown";
|
||||||
|
|
||||||
|
// Include the endpoint in the partition key for endpoint-specific limits
|
||||||
|
var endpoint = httpContext.Request.Path.ToString();
|
||||||
|
|
||||||
|
return $"{clientIp}:{endpoint}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? GetRetryAfter(OnRejectedContext context)
|
||||||
|
{
|
||||||
|
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
|
||||||
|
{
|
||||||
|
return (int)retryAfter.TotalSeconds;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using OpenCand.API.Config;
|
||||||
|
|
||||||
namespace OpenCand.API.Controllers
|
namespace OpenCand.API.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("v1/[controller]")]
|
[Route("v1/[controller]")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
|
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
|
||||||
public class BaseController : Controller
|
public class BaseController : Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.VisualBasic;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using OpenCand.API.Config;
|
||||||
using OpenCand.API.Model;
|
using OpenCand.API.Model;
|
||||||
using OpenCand.API.Services;
|
using OpenCand.API.Services;
|
||||||
using OpenCand.Core.Models;
|
using OpenCand.Core.Models;
|
||||||
|
using OpenCand.Core.Utils;
|
||||||
|
|
||||||
namespace OpenCand.API.Controllers
|
namespace OpenCand.API.Controllers
|
||||||
{
|
{
|
||||||
@@ -16,15 +18,37 @@ namespace OpenCand.API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("search")]
|
[HttpGet("search")]
|
||||||
|
[EnableRateLimiting(RateLimitingConfig.CandidatoSearchPolicy)]
|
||||||
public async Task<CandidatoSearchResult> CandidatoSearch([FromQuery] string q)
|
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}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<Candidato> GetCandidatoById([FromRoute] Guid 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")]
|
[HttpGet("{id}/bens")]
|
||||||
@@ -40,6 +64,7 @@ namespace OpenCand.API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/reveal-cpf")]
|
[HttpGet("{id}/reveal-cpf")]
|
||||||
|
[EnableRateLimiting(RateLimitingConfig.CpfRevealPolicy)]
|
||||||
public async Task<CpfRevealResult> GetCandidatoCpfById([FromRoute] Guid id)
|
public async Task<CpfRevealResult> GetCandidatoCpfById([FromRoute] Guid id)
|
||||||
{
|
{
|
||||||
var rnd = new Random();
|
var rnd = new Random();
|
||||||
@@ -47,5 +72,17 @@ namespace OpenCand.API.Controllers
|
|||||||
await Task.Delay(randomWait);
|
await Task.Delay(randomWait);
|
||||||
return await openCandService.GetCandidatoCpfById(id);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +1,12 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using OpenCand.API.Config;
|
||||||
using OpenCand.API.Services;
|
using OpenCand.API.Services;
|
||||||
using OpenCand.Core.Models;
|
using OpenCand.Core.Models;
|
||||||
|
|
||||||
namespace OpenCand.API.Controllers
|
namespace OpenCand.API.Controllers
|
||||||
{
|
{
|
||||||
|
[EnableRateLimiting(RateLimitingConfig.DefaultPolicy)]
|
||||||
public class StatsController : BaseController
|
public class StatsController : BaseController
|
||||||
{
|
{
|
||||||
private readonly OpenCandService openCandService;
|
private readonly OpenCandService openCandService;
|
||||||
@@ -18,5 +21,18 @@ namespace OpenCand.API.Controllers
|
|||||||
{
|
{
|
||||||
return await openCandService.GetOpenCandStatsAsync();
|
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 string Cpf { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class DespesasResult
|
||||||
|
{
|
||||||
|
public List<Despesa> Despesas { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReceitaResult
|
||||||
|
{
|
||||||
|
public List<Receita> Receitas { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,10 +5,9 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Npgsql" Version="8.0.2" />
|
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.3" />
|
||||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using OpenCand.API.Config;
|
using OpenCand.API.Config;
|
||||||
using OpenCand.API.Repository;
|
using OpenCand.API.Repository;
|
||||||
@@ -10,13 +11,14 @@ namespace OpenCand.API
|
|||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args); // Add services to the container.
|
||||||
|
|
||||||
// Add services to the container.
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
SetupServices(builder);
|
SetupServices(builder);
|
||||||
|
|
||||||
|
// Configure rate limiting
|
||||||
|
builder.Services.ConfigureRateLimiting();
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
@@ -35,9 +37,11 @@ namespace OpenCand.API
|
|||||||
FileProvider = new PhysicalFileProvider(Path.Combine(workingDir, "fotos_cand")),
|
FileProvider = new PhysicalFileProvider(Path.Combine(workingDir, "fotos_cand")),
|
||||||
RequestPath = "/assets/fotos"
|
RequestPath = "/assets/fotos"
|
||||||
});
|
});
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
// Use rate limiting middleware
|
||||||
|
app.UseRateLimiter();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
@@ -54,10 +58,25 @@ namespace OpenCand.API
|
|||||||
private static void SetupServices(WebApplicationBuilder builder)
|
private static void SetupServices(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
builder.Services.Configure<FotosSettings>(builder.Configuration.GetSection("FotosSettings"));
|
builder.Services.Configure<FotosSettings>(builder.Configuration.GetSection("FotosSettings"));
|
||||||
builder.Services.AddScoped<OpenCandRepository>();
|
|
||||||
builder.Services.AddScoped<CandidatoRepository>();
|
// Configure memory cache with size limit from appsettings
|
||||||
builder.Services.AddScoped<BemCandidatoRepository>();
|
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<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 Npgsql;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace OpenCand.Repository
|
namespace OpenCand.Repository
|
||||||
{
|
{
|
||||||
@@ -7,11 +9,153 @@ namespace OpenCand.Repository
|
|||||||
{
|
{
|
||||||
protected string ConnectionString { get; private set; }
|
protected string ConnectionString { get; private set; }
|
||||||
protected NpgsqlConnection? Connection { 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"] ??
|
ConnectionString = configuration["DatabaseSettings:ConnectionString"] ??
|
||||||
throw new ArgumentNullException("Connection string not found in configuration");
|
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 Dapper;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using OpenCand.Core.Models;
|
using OpenCand.Core.Models;
|
||||||
|
|
||||||
@@ -6,7 +7,7 @@ namespace OpenCand.Repository
|
|||||||
{
|
{
|
||||||
public class BemCandidatoRepository : BaseRepository
|
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 Dapper;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using OpenCand.Core.Models;
|
using OpenCand.Core.Models;
|
||||||
|
|
||||||
@@ -6,48 +7,44 @@ namespace OpenCand.Repository
|
|||||||
{
|
{
|
||||||
public class CandidatoRepository : BaseRepository
|
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>(@"
|
return (await connection.QueryAsync<Candidato>(@"
|
||||||
SELECT *,
|
SELECT c.*,
|
||||||
CASE
|
GREATEST(similarity(c.apelido, @q), similarity(c.nome, @q)) AS sim
|
||||||
WHEN lower(apelido) = lower(@query) THEN 0 -- apelido Exact match (case-insensitive)
|
FROM candidato c
|
||||||
WHEN lower(apelido) LIKE lower(@query) || '%' THEN 1 -- apelido Starts with match (case-insensitive)
|
WHERE c.apelido % @q
|
||||||
WHEN lower(apelido) LIKE '%' || lower(@query) THEN 2 -- apelido Contains anywhere match (case-insensitive)
|
OR c.nome % @q
|
||||||
WHEN lower(nome) = lower(@query) THEN 0 -- nome Exact match (case-insensitive)
|
ORDER BY c.popularidade DESC, sim DESC, length(c.nome) ASC
|
||||||
WHEN lower(nome) LIKE lower(@query) || '%' THEN 1 -- nome Starts with match (case-insensitive)
|
LIMIT 10;
|
||||||
WHEN lower(nome) LIKE '%' || lower(@query) THEN 2 -- nome Contains anywhere match (case-insensitive)
|
", new { q = query })).AsList();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Candidato?> GetCandidatoAsync(Guid idcandidato)
|
public async Task<Candidato?> GetCandidatoAsync(Guid idcandidato)
|
||||||
{
|
{
|
||||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
string cacheKey = GenerateCacheKey("Candidato", idcandidato);
|
||||||
|
|
||||||
|
return await GetOrSetCacheAsync(cacheKey, async () =>
|
||||||
{
|
{
|
||||||
return await connection.QueryFirstOrDefaultAsync<Candidato>(@"
|
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||||
SELECT * FROM candidato
|
{
|
||||||
WHERE idcandidato = @idcandidato;",
|
return await connection.QueryFirstOrDefaultAsync<Candidato>(@"
|
||||||
new { idcandidato });
|
SELECT * FROM candidato
|
||||||
}
|
WHERE idcandidato = @idcandidato;",
|
||||||
|
new { idcandidato });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> GetCandidatoCpfAsync(Guid idcandidato)
|
public async Task<string?> GetCandidatoCpfAsync(Guid idcandidato)
|
||||||
@@ -93,5 +90,37 @@ namespace OpenCand.Repository
|
|||||||
return (await connection.QueryAsync<RedeSocial>(query, new { idcandidato })).AsList();
|
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 Dapper;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using OpenCand.Core.Models;
|
using OpenCand.Core.Models;
|
||||||
using OpenCand.Repository;
|
using OpenCand.Repository;
|
||||||
@@ -7,23 +8,136 @@ namespace OpenCand.API.Repository
|
|||||||
{
|
{
|
||||||
public class OpenCandRepository : BaseRepository
|
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()
|
public async Task<OpenCandStats> GetOpenCandStatsAsync()
|
||||||
{
|
{
|
||||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
string cacheKey = GenerateCacheKey("OpenCandStats");
|
||||||
|
|
||||||
|
var result = await GetOrSetCacheAsync(cacheKey, async () =>
|
||||||
{
|
{
|
||||||
var stats = await connection.QueryFirstOrDefaultAsync<OpenCandStats>(@"
|
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||||
SELECT
|
{
|
||||||
(SELECT COUNT(idcandidato) FROM candidato) AS TotalCandidatos,
|
var stats = await connection.QueryFirstOrDefaultAsync<OpenCandStats>(@"
|
||||||
(SELECT COUNT(*) FROM bem_candidato) AS TotalBemCandidatos,
|
SELECT
|
||||||
(SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos,
|
(SELECT COUNT(idcandidato) FROM candidato) AS TotalCandidatos,
|
||||||
(SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais,
|
(SELECT COUNT(*) FROM bem_candidato) AS TotalBemCandidatos,
|
||||||
(SELECT COUNT(DISTINCT ano) FROM candidato_mapping) AS TotalEleicoes;");
|
(SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos,
|
||||||
return stats ?? new OpenCandStats();
|
(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 OpenCandRepository openCandRepository;
|
||||||
private readonly CandidatoRepository candidatoRepository;
|
private readonly CandidatoRepository candidatoRepository;
|
||||||
private readonly BemCandidatoRepository bemCandidatoRepository;
|
private readonly BemCandidatoRepository bemCandidatoRepository;
|
||||||
|
private readonly DespesaReceitaRepository despesaReceitaRepository;
|
||||||
private readonly IConfiguration configuration;
|
private readonly IConfiguration configuration;
|
||||||
private readonly FotosSettings fotoSettings;
|
private readonly FotosSettings fotoSettings;
|
||||||
private readonly ILogger<OpenCandService> logger;
|
private readonly ILogger<OpenCandService> logger;
|
||||||
@@ -20,6 +21,7 @@ namespace OpenCand.API.Services
|
|||||||
OpenCandRepository openCandRepository,
|
OpenCandRepository openCandRepository,
|
||||||
CandidatoRepository candidatoRepository,
|
CandidatoRepository candidatoRepository,
|
||||||
BemCandidatoRepository bemCandidatoRepository,
|
BemCandidatoRepository bemCandidatoRepository,
|
||||||
|
DespesaReceitaRepository despesaReceitaRepository,
|
||||||
IOptions<FotosSettings> fotoSettings,
|
IOptions<FotosSettings> fotoSettings,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<OpenCandService> logger)
|
ILogger<OpenCandService> logger)
|
||||||
@@ -27,6 +29,7 @@ namespace OpenCand.API.Services
|
|||||||
this.openCandRepository = openCandRepository;
|
this.openCandRepository = openCandRepository;
|
||||||
this.candidatoRepository = candidatoRepository;
|
this.candidatoRepository = candidatoRepository;
|
||||||
this.bemCandidatoRepository = bemCandidatoRepository;
|
this.bemCandidatoRepository = bemCandidatoRepository;
|
||||||
|
this.despesaReceitaRepository = despesaReceitaRepository;
|
||||||
this.fotoSettings = fotoSettings.Value;
|
this.fotoSettings = fotoSettings.Value;
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
@@ -34,25 +37,62 @@ namespace OpenCand.API.Services
|
|||||||
|
|
||||||
public async Task<OpenCandStats> GetOpenCandStatsAsync()
|
public async Task<OpenCandStats> GetOpenCandStatsAsync()
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Getting OpenCand stats");
|
||||||
|
|
||||||
return await openCandRepository.GetOpenCandStatsAsync();
|
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)
|
public async Task<CandidatoSearchResult> SearchCandidatosAsync(string query)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation($"Searching candidatos with query: {query}");
|
||||||
|
|
||||||
return new CandidatoSearchResult()
|
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)
|
public async Task<Candidato> GetCandidatoAsync(Guid idcandidato)
|
||||||
{
|
{
|
||||||
var result = await candidatoRepository.GetCandidatoAsync(idcandidato);
|
var result = await candidatoRepository.GetCandidatoAsync(idcandidato);
|
||||||
var eleicoes = await candidatoRepository.GetCandidatoMappingById(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)
|
if (result == null)
|
||||||
@@ -63,11 +103,23 @@ namespace OpenCand.API.Services
|
|||||||
{
|
{
|
||||||
throw new KeyNotFoundException($"CandidatoMapping for ID {idcandidato} not found.");
|
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();
|
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.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.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;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -80,6 +132,8 @@ namespace OpenCand.API.Services
|
|||||||
result = new List<BemCandidato>();
|
result = new List<BemCandidato>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogInformation($"Found {result.Count} bens for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
return new BemCandidatoResult()
|
return new BemCandidatoResult()
|
||||||
{
|
{
|
||||||
Bens = result.OrderByDescending(r => r.TipoBem).ThenByDescending(r => r.Valor).ToList()
|
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)
|
public async Task<RedeSocialResult> GetCandidatoRedeSocialById(Guid idcandidato)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation($"Getting redes sociais for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
var result = await candidatoRepository.GetCandidatoRedeSocialById(idcandidato);
|
var result = await candidatoRepository.GetCandidatoRedeSocialById(idcandidato);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
result = new List<RedeSocial>();
|
result = new List<RedeSocial>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogDebug($"Found {result.Count} redes sociais for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
return new RedeSocialResult()
|
return new RedeSocialResult()
|
||||||
{
|
{
|
||||||
RedesSociais = result.OrderByDescending(r => r.Ano).ToList()
|
RedesSociais = result.OrderByDescending(r => r.Ano).ToList()
|
||||||
@@ -102,16 +160,56 @@ namespace OpenCand.API.Services
|
|||||||
|
|
||||||
public async Task<CpfRevealResult> GetCandidatoCpfById(Guid idcandidato)
|
public async Task<CpfRevealResult> GetCandidatoCpfById(Guid idcandidato)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation($"Getting CPF for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
var result = await candidatoRepository.GetCandidatoCpfAsync(idcandidato);
|
var result = await candidatoRepository.GetCandidatoCpfAsync(idcandidato);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
return new CpfRevealResult();
|
return new CpfRevealResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogDebug($"Found CPF {result} for Candidato ID {idcandidato}");
|
||||||
|
|
||||||
return new CpfRevealResult()
|
return new CpfRevealResult()
|
||||||
{
|
{
|
||||||
Cpf = result
|
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": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Debug",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Information"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DatabaseSettings": {
|
"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": {
|
"FotosSettings": {
|
||||||
"Path": "./fotos_cand",
|
"Path": "./fotos_cand",
|
||||||
"ApiBasePath": "http://localhost:5299/assets/fotos"
|
"ApiBasePath": "http://localhost:5299/assets/fotos"
|
||||||
},
|
},
|
||||||
|
"CacheSettings": {
|
||||||
|
"SizeLimitMB": 15
|
||||||
|
},
|
||||||
"AllowedHosts": "*"
|
"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 Sexo { get; set; }
|
||||||
|
|
||||||
public string EstadoCivil { get; set; }
|
public string Localidade { get; set; }
|
||||||
|
|
||||||
public string Escolaridade { get; set; }
|
public int Ultimoano { get; set; }
|
||||||
|
|
||||||
public string Ocupacao { get; set; }
|
|
||||||
|
|
||||||
public List<CandidatoMapping> Eleicoes { get; set; }
|
public List<CandidatoMapping> Eleicoes { get; set; }
|
||||||
|
|
||||||
|
public List<CandidatoExt> CandidatoExt { get; set; }
|
||||||
|
|
||||||
// API ONLY
|
// API ONLY
|
||||||
public string FotoUrl { get; set; }
|
public string FotoUrl { get; set; }
|
||||||
}
|
}
|
||||||
@@ -37,9 +37,9 @@ namespace OpenCand.Core.Models
|
|||||||
public Guid IdCandidato { get; set; }
|
public Guid IdCandidato { get; set; }
|
||||||
public string Cpf { get; set; }
|
public string Cpf { get; set; }
|
||||||
public string Nome { get; set; }
|
public string Nome { get; set; }
|
||||||
public string Apelido { get; set; }
|
|
||||||
public string SqCandidato { get; set; }
|
public string SqCandidato { get; set; }
|
||||||
public int Ano { get; set; }
|
public int Ano { get; set; }
|
||||||
|
public string Turno { get; set; }
|
||||||
public string TipoEleicao { get; set; }
|
public string TipoEleicao { get; set; }
|
||||||
public string SiglaUF { get; set; }
|
public string SiglaUF { get; set; }
|
||||||
public string NomeUE { 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 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 class RedeSocial
|
||||||
{
|
{
|
||||||
public Guid IdCandidato { get; set; }
|
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 TotalRedesSociais { get; set; }
|
||||||
public long TotalEleicoes { 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 CandidatosFolder { get; set; } = string.Empty;
|
||||||
public string BensCandidatosFolder { get; set; } = string.Empty;
|
public string BensCandidatosFolder { get; set; } = string.Empty;
|
||||||
public string RedesSociaisFolder { 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,11 +7,11 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
|
||||||
<PackageReference Include="Npgsql" Version="8.0.2" />
|
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -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)
|
if (columns.Length > headerCount)
|
||||||
{
|
{
|
||||||
logger.LogCritical($"FixCsvFile - Line {i + 1} has {columns.Length} columns, expected {headerCount}. Halting process.");
|
// 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
|
// 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}]...");
|
logger.LogWarning($"FixCsvFile - Line {i + 1} has {columns.Length} columns, expected {headerCount}. Attempting to fix [i = {lineJump}]...");
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using CsvHelper;
|
using CsvHelper;
|
||||||
using CsvHelper.Configuration;
|
using CsvHelper.Configuration;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using OpenCand.ETL.Contracts;
|
using OpenCand.ETL.Contracts;
|
||||||
|
|
||||||
@@ -11,9 +12,12 @@ namespace OpenCand.Parser.Services
|
|||||||
private readonly ILogger<CsvParserService<CsvObj>> logger;
|
private readonly ILogger<CsvParserService<CsvObj>> logger;
|
||||||
private readonly CsvFixerService csvFixerService;
|
private readonly CsvFixerService csvFixerService;
|
||||||
private readonly IParserService<CsvObj> parserService;
|
private readonly IParserService<CsvObj> parserService;
|
||||||
|
private readonly IConfiguration configuration;
|
||||||
|
|
||||||
private readonly CsvConfiguration parserConfig;
|
private readonly CsvConfiguration parserConfig;
|
||||||
|
|
||||||
|
private readonly int MaxDegreeOfParallelism;
|
||||||
|
|
||||||
// Progress tracking fields
|
// Progress tracking fields
|
||||||
private long processedCount;
|
private long processedCount;
|
||||||
private long totalCount;
|
private long totalCount;
|
||||||
@@ -24,11 +28,26 @@ namespace OpenCand.Parser.Services
|
|||||||
public CsvParserService(
|
public CsvParserService(
|
||||||
ILogger<CsvParserService<CsvObj>> logger,
|
ILogger<CsvParserService<CsvObj>> logger,
|
||||||
IParserService<CsvObj> parserService,
|
IParserService<CsvObj> parserService,
|
||||||
CsvFixerService csvFixerService)
|
CsvFixerService csvFixerService,
|
||||||
|
IConfiguration configuration)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.csvFixerService = csvFixerService;
|
this.csvFixerService = csvFixerService;
|
||||||
this.parserService = parserService;
|
this.parserService = parserService;
|
||||||
|
this.configuration = configuration;
|
||||||
|
|
||||||
|
var defaultThreadCount = configuration.GetValue<int>("ParserSettings:DefaultThreads", 25);
|
||||||
|
|
||||||
|
if (configuration.GetValue<string>($"ParserSettings:{typeof(CsvObj).Name}Threads") == null)
|
||||||
|
{
|
||||||
|
logger.LogInformation($"ParserSettings:{typeof(CsvObj).Name}Threads not found in configuration, using default value of {defaultThreadCount}.");
|
||||||
|
MaxDegreeOfParallelism = configuration.GetValue<int>("ParserSettings:DefaultThreads", defaultThreadCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = configuration.GetValue<int>($"ParserSettings:{typeof(CsvObj).Name}Threads", defaultThreadCount);
|
||||||
|
logger.LogInformation($"Using {MaxDegreeOfParallelism} threads for parsing {typeof(CsvObj).Name} based on configuration.");
|
||||||
|
}
|
||||||
|
|
||||||
parserConfig = new CsvConfiguration(CultureInfo.InvariantCulture)
|
parserConfig = new CsvConfiguration(CultureInfo.InvariantCulture)
|
||||||
{
|
{
|
||||||
@@ -60,7 +79,7 @@ namespace OpenCand.Parser.Services
|
|||||||
using var csv = new CsvReader(reader, parserConfig);
|
using var csv = new CsvReader(reader, parserConfig);
|
||||||
var po = new ParallelOptions
|
var po = new ParallelOptions
|
||||||
{
|
{
|
||||||
MaxDegreeOfParallelism = 40
|
MaxDegreeOfParallelism = MaxDegreeOfParallelism
|
||||||
};
|
};
|
||||||
|
|
||||||
//csv.Context.RegisterClassMap<ClassMap<CsvObj>>(); // optional for advanced mapping, not needed
|
//csv.Context.RegisterClassMap<ClassMap<CsvObj>>(); // optional for advanced mapping, not needed
|
||||||
|
@@ -11,6 +11,9 @@ namespace OpenCand.Parser.Models
|
|||||||
[Name("TP_ABRANGENCIA")]
|
[Name("TP_ABRANGENCIA")]
|
||||||
public string TipoAbrangencia { get; set; }
|
public string TipoAbrangencia { get; set; }
|
||||||
|
|
||||||
|
[Name("NR_TURNO")]
|
||||||
|
public string Turno { get; set; }
|
||||||
|
|
||||||
[Name("SG_UF")]
|
[Name("SG_UF")]
|
||||||
public string SiglaUF { get; set; }
|
public string SiglaUF { get; set; }
|
||||||
|
|
||||||
@@ -33,10 +36,10 @@ namespace OpenCand.Parser.Models
|
|||||||
public string Apelido { get; set; }
|
public string Apelido { get; set; }
|
||||||
|
|
||||||
[Name("NR_CPF_CANDIDATO")]
|
[Name("NR_CPF_CANDIDATO")]
|
||||||
public string CPFCandidato { get; set; }
|
public string? CPFCandidato { get; set; }
|
||||||
|
|
||||||
[Name("DS_EMAIL", "NM_EMAIL")]
|
[Name("DS_EMAIL", "NM_EMAIL")]
|
||||||
public string Email { get; set; }
|
public string? Email { get; set; }
|
||||||
|
|
||||||
[Name("DT_NASCIMENTO")]
|
[Name("DT_NASCIMENTO")]
|
||||||
public string DataNascimento { get; set; }
|
public string DataNascimento { get; set; }
|
||||||
@@ -54,7 +57,7 @@ namespace OpenCand.Parser.Models
|
|||||||
public string GrauInstrucao { get; set; }
|
public string GrauInstrucao { get; set; }
|
||||||
|
|
||||||
[Name("DS_SIT_TOT_TURNO")]
|
[Name("DS_SIT_TOT_TURNO")]
|
||||||
public string SituacaoTurno { get; set; }
|
public string? SituacaoTurno { get; set; }
|
||||||
|
|
||||||
[Name("NR_PARTIDO")]
|
[Name("NR_PARTIDO")]
|
||||||
public int NumeroPartido { get; set; }
|
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.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using OpenCand.Config;
|
using OpenCand.Config;
|
||||||
|
using OpenCand.ETL.Repository;
|
||||||
|
using OpenCand.ETL.Services;
|
||||||
using OpenCand.Parser.Models;
|
using OpenCand.Parser.Models;
|
||||||
using OpenCand.Parser.Services;
|
using OpenCand.Parser.Services;
|
||||||
|
|
||||||
@@ -15,6 +17,12 @@ namespace OpenCand.Parser
|
|||||||
private readonly CsvParserService<CandidatoCSV> candidatoParserService;
|
private readonly CsvParserService<CandidatoCSV> candidatoParserService;
|
||||||
private readonly CsvParserService<BemCandidatoCSV> bemCandidatoParserService;
|
private readonly CsvParserService<BemCandidatoCSV> bemCandidatoParserService;
|
||||||
private readonly CsvParserService<RedeSocialCSV> redeSocialParserService;
|
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;
|
private readonly string BasePath;
|
||||||
|
|
||||||
@@ -24,7 +32,11 @@ namespace OpenCand.Parser
|
|||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
CsvParserService<CandidatoCSV> candidatoParserService,
|
CsvParserService<CandidatoCSV> candidatoParserService,
|
||||||
CsvParserService<BemCandidatoCSV> bemCandidatoParserService,
|
CsvParserService<BemCandidatoCSV> bemCandidatoParserService,
|
||||||
CsvParserService<RedeSocialCSV> redeSocialParserService)
|
CsvParserService<RedeSocialCSV> redeSocialParserService,
|
||||||
|
CsvParserService<DespesasCSV> despesaParserService,
|
||||||
|
CsvParserService<ReceitasCSV> receitaParserService,
|
||||||
|
DespesaReceitaService despesaReceitaService,
|
||||||
|
ViewRepository viewRepository)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.csvSettings = csvSettings.Value;
|
this.csvSettings = csvSettings.Value;
|
||||||
@@ -32,6 +44,10 @@ namespace OpenCand.Parser
|
|||||||
this.candidatoParserService = candidatoParserService;
|
this.candidatoParserService = candidatoParserService;
|
||||||
this.bemCandidatoParserService = bemCandidatoParserService;
|
this.bemCandidatoParserService = bemCandidatoParserService;
|
||||||
this.redeSocialParserService = redeSocialParserService;
|
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
|
// Get the base path from either SampleFolder in csvSettings or the BasePath in configuration
|
||||||
BasePath = configuration.GetValue<string>("BasePath") ?? string.Empty;
|
BasePath = configuration.GetValue<string>("BasePath") ?? string.Empty;
|
||||||
@@ -49,12 +65,25 @@ namespace OpenCand.Parser
|
|||||||
var candidatosDirectory = Path.Combine(BasePath, csvSettings.CandidatosFolder);
|
var candidatosDirectory = Path.Combine(BasePath, csvSettings.CandidatosFolder);
|
||||||
var bensCandidatosDirectory = Path.Combine(BasePath, csvSettings.BensCandidatosFolder);
|
var bensCandidatosDirectory = Path.Combine(BasePath, csvSettings.BensCandidatosFolder);
|
||||||
var redesSociaisDirectory = Path.Combine(BasePath, csvSettings.RedesSociaisFolder);
|
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(candidatosDirectory, candidatoParserService);
|
||||||
await ParseFolder(bensCandidatosDirectory, bemCandidatoParserService);
|
await ParseFolder(bensCandidatosDirectory, bemCandidatoParserService);
|
||||||
await ParseFolder(redesSociaisDirectory, redeSocialParserService);
|
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 - 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)
|
private async Task ParseFolder<CsvObj>(string csvDirectory, CsvParserService<CsvObj> csvParserService)
|
||||||
|
@@ -5,6 +5,7 @@ using OpenCand.ETL.Contracts;
|
|||||||
using OpenCand.Parser.Models;
|
using OpenCand.Parser.Models;
|
||||||
using OpenCand.Services;
|
using OpenCand.Services;
|
||||||
using OpenCand.Parser.Services;
|
using OpenCand.Parser.Services;
|
||||||
|
using OpenCand.ETL.Extensions;
|
||||||
|
|
||||||
namespace OpenCand.ETL.Parser.ParserServices
|
namespace OpenCand.ETL.Parser.ParserServices
|
||||||
{
|
{
|
||||||
@@ -25,7 +26,7 @@ namespace OpenCand.ETL.Parser.ParserServices
|
|||||||
{
|
{
|
||||||
// Parse decimal value
|
// Parse decimal value
|
||||||
decimal? valor = null;
|
decimal? valor = null;
|
||||||
if (!string.IsNullOrEmpty(record.ValorBemCandidato))
|
if (!record.ValorBemCandidato.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
string normalizedValue = record.ValorBemCandidato.Replace(".", "").Replace(",", ".");
|
string normalizedValue = record.ValorBemCandidato.Replace(".", "").Replace(",", ".");
|
||||||
if (decimal.TryParse(normalizedValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedValue))
|
if (decimal.TryParse(normalizedValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedValue))
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using OpenCand.Core.Models;
|
using OpenCand.Core.Models;
|
||||||
using OpenCand.ETL.Contracts;
|
using OpenCand.ETL.Contracts;
|
||||||
|
using OpenCand.ETL.Extensions;
|
||||||
using OpenCand.Parser.Models;
|
using OpenCand.Parser.Models;
|
||||||
using OpenCand.Services;
|
using OpenCand.Services;
|
||||||
|
|
||||||
@@ -22,61 +23,80 @@ namespace OpenCand.ETL.Parser.ParserServices
|
|||||||
|
|
||||||
public async Task ParseObject(CandidatoCSV record)
|
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
|
record.CPFCandidato = null; // Handle null/empty/whitespace CPF
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
record.CPFCandidato = record.CPFCandidato.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (record.NomeCandidato == "NÃO DIVULGÁVEL" ||
|
if (record.Apelido.IsNullOrEmpty())
|
||||||
string.IsNullOrEmpty(record.NomeCandidato) ||
|
{
|
||||||
record.NomeCandidato == "#NULO")
|
record.Apelido = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.NomeCandidato.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
logger.LogCritical($"ParseCandidatosAsync - Candidate with id {record.SequencialCandidato} with invalid name, skipping...");
|
logger.LogCritical($"ParseCandidatosAsync - Candidate with id {record.SequencialCandidato} with invalid name, skipping...");
|
||||||
return; // Skip candidates with invalid name
|
return; // Skip candidates with invalid name
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(record.Apelido) ||
|
if (record.Apelido.IsNullOrEmpty())
|
||||||
record.Apelido.Contains("#NUL") ||
|
|
||||||
record.Apelido.Contains("NULO#") ||
|
|
||||||
record.Apelido.Contains("#NE"))
|
|
||||||
{
|
{
|
||||||
record.Apelido = null;
|
record.Apelido = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (record.SituacaoTurno.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
record.SituacaoTurno = null;
|
||||||
|
}
|
||||||
|
|
||||||
var candidato = new Candidato
|
var candidato = new Candidato
|
||||||
{
|
{
|
||||||
Cpf = record.CPFCandidato,
|
Cpf = record.CPFCandidato,
|
||||||
SqCandidato = record.SequencialCandidato,
|
SqCandidato = record.SequencialCandidato,
|
||||||
Nome = record.NomeCandidato,
|
Nome = record.NomeCandidato.Trim(),
|
||||||
Apelido = record.Apelido,
|
Apelido = record.Apelido?.Trim(),
|
||||||
Email = record.Email.Contains("@") ? record.Email : null,
|
Sexo = record.Genero.Trim(),
|
||||||
Sexo = record.Genero,
|
Localidade = record.NomeUE.Trim(),
|
||||||
EstadoCivil = record.EstadoCivil,
|
Ultimoano = record.AnoEleicao,
|
||||||
Escolaridade = record.GrauInstrucao,
|
|
||||||
Ocupacao = record.Ocupacao,
|
|
||||||
Eleicoes = new List<CandidatoMapping>()
|
Eleicoes = new List<CandidatoMapping>()
|
||||||
{
|
{
|
||||||
new CandidatoMapping
|
new CandidatoMapping
|
||||||
{
|
{
|
||||||
Cpf = record.CPFCandidato,
|
Cpf = record.CPFCandidato,
|
||||||
Nome = record.NomeCandidato,
|
Nome = record.NomeCandidato.Trim(),
|
||||||
Apelido = record.Apelido,
|
SqCandidato = record.SequencialCandidato.Trim(),
|
||||||
SqCandidato = record.SequencialCandidato,
|
Ano = record.AnoEleicao,
|
||||||
Ano = record.AnoEleicao,
|
Turno = record.Turno.Trim(),
|
||||||
TipoEleicao = record.TipoAbrangencia,
|
TipoEleicao = record.TipoAbrangencia.Trim(),
|
||||||
NomeUE = record.NomeUE,
|
NomeUE = record.NomeUE.Trim(),
|
||||||
SiglaUF = record.SiglaUF,
|
SiglaUF = record.SiglaUF.Trim(),
|
||||||
Cargo = record.DescricaoCargo,
|
Cargo = record.DescricaoCargo.Trim(),
|
||||||
NrCandidato = record.NumeroCandidato,
|
NrCandidato = record.NumeroCandidato.Trim(),
|
||||||
Resultado = record.SituacaoTurno,
|
Resultado = record.SituacaoTurno?.Trim() ?? "-",
|
||||||
Partido = new Partido
|
Partido = new Partido
|
||||||
{
|
{
|
||||||
Sigla = record.SiglaPartido,
|
Sigla = record.SiglaPartido.Trim(),
|
||||||
Nome = record.NomePartido,
|
Nome = record.NomePartido.Trim(),
|
||||||
Numero = record.NumeroPartido,
|
Numero = record.NumeroPartido,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
CandidatoExt = new List<CandidatoExt>()
|
||||||
|
{
|
||||||
|
new CandidatoExt
|
||||||
|
{
|
||||||
|
Apelido = record.Apelido?.Trim(),
|
||||||
|
EstadoCivil = record.EstadoCivil.Trim(),
|
||||||
|
Escolaridade = record.GrauInstrucao.Trim(),
|
||||||
|
Ocupacao = record.Ocupacao.Trim(),
|
||||||
|
Ano = record.AnoEleicao,
|
||||||
|
Email = record.Email.IsNullOrEmpty() ? null : record.Email.Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(record.DataNascimento) &&
|
if (!string.IsNullOrEmpty(record.DataNascimento) &&
|
||||||
|
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.Contracts;
|
||||||
using OpenCand.ETL.Parser.ParserServices;
|
using OpenCand.ETL.Parser.ParserServices;
|
||||||
using OpenCand.ETL.Repository;
|
using OpenCand.ETL.Repository;
|
||||||
|
using OpenCand.ETL.Services;
|
||||||
using OpenCand.Parser;
|
using OpenCand.Parser;
|
||||||
using OpenCand.Parser.Models;
|
using OpenCand.Parser.Models;
|
||||||
using OpenCand.Parser.Services;
|
using OpenCand.Parser.Services;
|
||||||
@@ -60,18 +61,25 @@ namespace OpenCand
|
|||||||
services.AddTransient<IParserService<CandidatoCSV>, CandidatoParserService>();
|
services.AddTransient<IParserService<CandidatoCSV>, CandidatoParserService>();
|
||||||
services.AddTransient<IParserService<BemCandidatoCSV>, BemCandidatoParserService>();
|
services.AddTransient<IParserService<BemCandidatoCSV>, BemCandidatoParserService>();
|
||||||
services.AddTransient<IParserService<RedeSocialCSV>, RedeSocialParserService>();
|
services.AddTransient<IParserService<RedeSocialCSV>, RedeSocialParserService>();
|
||||||
|
services.AddTransient<IParserService<DespesasCSV>, DespesaParserService>();
|
||||||
|
services.AddTransient<IParserService<ReceitasCSV>, ReceitaParserService>();
|
||||||
services.AddTransient<CsvParserService<CandidatoCSV>>();
|
services.AddTransient<CsvParserService<CandidatoCSV>>();
|
||||||
services.AddTransient<CsvParserService<BemCandidatoCSV>>();
|
services.AddTransient<CsvParserService<BemCandidatoCSV>>();
|
||||||
services.AddTransient<CsvParserService<RedeSocialCSV>>();
|
services.AddTransient<CsvParserService<RedeSocialCSV>>();
|
||||||
|
services.AddTransient<CsvParserService<DespesasCSV>>();
|
||||||
|
services.AddTransient<CsvParserService<ReceitasCSV>>();
|
||||||
services.AddTransient<ParserManager>();
|
services.AddTransient<ParserManager>();
|
||||||
|
services.AddTransient<DespesaReceitaService>();
|
||||||
services.AddTransient<CandidatoService>();
|
services.AddTransient<CandidatoService>();
|
||||||
services.AddTransient<BemCandidatoService>();
|
services.AddTransient<BemCandidatoService>();
|
||||||
services.AddTransient<RedeSocialService>();
|
services.AddTransient<RedeSocialService>();
|
||||||
|
services.AddTransient<DespesaReceitaRepository>();
|
||||||
services.AddTransient<CandidatoRepository>();
|
services.AddTransient<CandidatoRepository>();
|
||||||
services.AddTransient<BemCandidatoRepository>();
|
services.AddTransient<BemCandidatoRepository>();
|
||||||
services.AddTransient<RedeSocialRepository>();
|
services.AddTransient<RedeSocialRepository>();
|
||||||
services.AddTransient<PartidoRepository>();
|
services.AddTransient<PartidoRepository>();
|
||||||
services.AddTransient<CsvFixerService>();
|
services.AddTransient<CsvFixerService>();
|
||||||
|
services.AddTransient<ViewRepository>();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,18 +16,17 @@ namespace OpenCand.Repository
|
|||||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||||
{
|
{
|
||||||
await connection.ExecuteAsync(@"
|
await connection.ExecuteAsync(@"
|
||||||
INSERT INTO candidato (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, @email, @sexo, @estadocivil, @escolaridade, @ocupacao)
|
VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @sexo, @ultimoano, @localidade)
|
||||||
ON CONFLICT (idcandidato) DO UPDATE SET
|
ON CONFLICT (idcandidato) DO UPDATE SET
|
||||||
cpf = EXCLUDED.cpf,
|
cpf = EXCLUDED.cpf,
|
||||||
nome = EXCLUDED.nome,
|
nome = EXCLUDED.nome,
|
||||||
|
apelido = EXCLUDED.apelido,
|
||||||
datanascimento = EXCLUDED.datanascimento,
|
datanascimento = EXCLUDED.datanascimento,
|
||||||
email = EXCLUDED.email,
|
|
||||||
sexo = EXCLUDED.sexo,
|
sexo = EXCLUDED.sexo,
|
||||||
estadocivil = EXCLUDED.estadocivil,
|
localidade = EXCLUDED.localidade,
|
||||||
escolaridade = EXCLUDED.escolaridade,
|
ultimoano = EXCLUDED.ultimoano
|
||||||
ocupacao = EXCLUDED.ocupacao,
|
WHERE candidato.ultimoano IS NULL OR EXCLUDED.ultimoano > candidato.ultimoano;",
|
||||||
apelido = EXCLUDED.apelido;",
|
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
idcandidato = candidato.IdCandidato,
|
idcandidato = candidato.IdCandidato,
|
||||||
@@ -35,11 +34,9 @@ namespace OpenCand.Repository
|
|||||||
nome = candidato.Nome,
|
nome = candidato.Nome,
|
||||||
apelido = candidato.Apelido,
|
apelido = candidato.Apelido,
|
||||||
datanascimento = candidato.DataNascimento,
|
datanascimento = candidato.DataNascimento,
|
||||||
email = candidato.Email,
|
|
||||||
sexo = candidato.Sexo,
|
sexo = candidato.Sexo,
|
||||||
estadocivil = candidato.EstadoCivil,
|
localidade = candidato.Localidade,
|
||||||
escolaridade = candidato.Escolaridade,
|
ultimoano = candidato.Ultimoano
|
||||||
ocupacao = candidato.Ocupacao
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,17 +46,17 @@ namespace OpenCand.Repository
|
|||||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
using (var connection = new NpgsqlConnection(ConnectionString))
|
||||||
{
|
{
|
||||||
await connection.ExecuteAsync(@"
|
await connection.ExecuteAsync(@"
|
||||||
INSERT INTO candidato_mapping (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, @apelido, @sqcandidato, @ano, @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;",
|
ON CONFLICT DO NOTHING;",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
idcandidato = candidatoMapping.IdCandidato,
|
idcandidato = candidatoMapping.IdCandidato,
|
||||||
cpf = candidatoMapping.Cpf,
|
cpf = candidatoMapping.Cpf,
|
||||||
nome = candidatoMapping.Nome,
|
nome = candidatoMapping.Nome,
|
||||||
apelido = candidatoMapping.Apelido,
|
|
||||||
sqcandidato = candidatoMapping.SqCandidato,
|
sqcandidato = candidatoMapping.SqCandidato,
|
||||||
ano = candidatoMapping.Ano,
|
ano = candidatoMapping.Ano,
|
||||||
|
turno = candidatoMapping.Turno,
|
||||||
tipoeleicao = candidatoMapping.TipoEleicao,
|
tipoeleicao = candidatoMapping.TipoEleicao,
|
||||||
siglauf = candidatoMapping.SiglaUF,
|
siglauf = candidatoMapping.SiglaUF,
|
||||||
nomeue = candidatoMapping.NomeUE,
|
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)
|
public async Task<Candidato?> GetCandidatoByCpf(string cpf)
|
||||||
{
|
{
|
||||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
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)
|
public async Task<Candidato?> GetCandidatoByNome(string nome, DateTime datanascimento)
|
||||||
{
|
{
|
||||||
using (var connection = new NpgsqlConnection(ConnectionString))
|
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)
|
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");
|
throw new ArgumentNullException(nameof(candidato), "Candidato cannot be null");
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ namespace OpenCand.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var candidatoMapping = candidato.Eleicoes.First();
|
var candidatoMapping = candidato.Eleicoes.First();
|
||||||
|
var candidatoExt = candidato.CandidatoExt.First();
|
||||||
|
|
||||||
// Add partido data
|
// Add partido data
|
||||||
if (candidatoMapping.Partido != null)
|
if (candidatoMapping.Partido != null)
|
||||||
@@ -52,18 +53,18 @@ namespace OpenCand.Services
|
|||||||
candidato.IdCandidato = existingCandidato.IdCandidato;
|
candidato.IdCandidato = existingCandidato.IdCandidato;
|
||||||
candidato.Cpf = GetNonEmptyString(existingCandidato.Cpf, candidato.Cpf);
|
candidato.Cpf = GetNonEmptyString(existingCandidato.Cpf, candidato.Cpf);
|
||||||
candidato.Email = GetNonEmptyString(existingCandidato.Email, candidato.Email);
|
candidato.Email = GetNonEmptyString(existingCandidato.Email, candidato.Email);
|
||||||
candidato.EstadoCivil = GetNonEmptyString(existingCandidato.EstadoCivil, candidato.EstadoCivil);
|
|
||||||
candidato.Apelido = GetNonEmptyString(existingCandidato.Apelido, candidato.Apelido);
|
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);
|
candidato.Sexo = GetNonEmptyString(existingCandidato.Sexo, candidato.Sexo);
|
||||||
|
|
||||||
candidatoMapping.IdCandidato = candidato.IdCandidato;
|
candidatoMapping.IdCandidato = candidato.IdCandidato;
|
||||||
candidatoMapping.Cpf = GetNonEmptyString(candidato.Cpf, candidatoMapping.Cpf);
|
candidatoMapping.Cpf = GetNonEmptyString(candidato.Cpf, candidatoMapping.Cpf);
|
||||||
|
|
||||||
|
candidatoExt.IdCandidato = candidato.IdCandidato;
|
||||||
|
|
||||||
// Update the entries for the existing candidate
|
// Update the entries for the existing candidate
|
||||||
await candidatoRepository.AddCandidatoAsync(candidato);
|
await candidatoRepository.AddCandidatoAsync(candidato);
|
||||||
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
|
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
|
||||||
|
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +80,12 @@ namespace OpenCand.Services
|
|||||||
if (existingMapping != null)
|
if (existingMapping != null)
|
||||||
{
|
{
|
||||||
candidato.IdCandidato = existingMapping.IdCandidato;
|
candidato.IdCandidato = existingMapping.IdCandidato;
|
||||||
candidato.Cpf = GetNonEmptyString(existingMapping.Cpf, candidato.Cpf);
|
candidatoExt.IdCandidato = existingMapping.IdCandidato;
|
||||||
candidato.Apelido = GetNonEmptyString(existingMapping.Apelido, candidato.Apelido);
|
|
||||||
await candidatoRepository.AddCandidatoAsync(candidato);
|
|
||||||
|
|
||||||
|
candidato.Cpf = GetNonEmptyString(existingMapping.Cpf, candidato.Cpf);
|
||||||
|
|
||||||
|
await candidatoRepository.AddCandidatoAsync(candidato);
|
||||||
|
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,8 +97,11 @@ namespace OpenCand.Services
|
|||||||
candidatoMapping.Cpf = candidato.Cpf;
|
candidatoMapping.Cpf = candidato.Cpf;
|
||||||
candidatoMapping.Nome = candidato.Nome;
|
candidatoMapping.Nome = candidato.Nome;
|
||||||
|
|
||||||
|
candidatoExt.IdCandidato = candidato.IdCandidato;
|
||||||
|
|
||||||
await candidatoRepository.AddCandidatoAsync(candidato);
|
await candidatoRepository.AddCandidatoAsync(candidato);
|
||||||
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
|
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
|
||||||
|
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetNonEmptyString(string? value1, string? value2)
|
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.IdCandidato = candidato.IdCandidato;
|
||||||
redeSocial.Rede = GetRedeSocialType(redeSocial.Link);
|
redeSocial.Rede = GetRedeSocialType(redeSocial.Link.Trim());
|
||||||
|
|
||||||
await redeSocialRepository.AddRedeSocialAsync(redeSocial);
|
await redeSocialRepository.AddRedeSocialAsync(redeSocial);
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,15 @@
|
|||||||
"CsvSettings": {
|
"CsvSettings": {
|
||||||
"CandidatosFolder": "data/consulta_cand",
|
"CandidatosFolder": "data/consulta_cand",
|
||||||
"BensCandidatosFolder": "data/bem_candidato",
|
"BensCandidatosFolder": "data/bem_candidato",
|
||||||
"RedesSociaisFolder": "data/rede_social"
|
"RedesSociaisFolder": "data/rede_social",
|
||||||
|
"DespesaCandidatoFolder": "data/despesas_candidato",
|
||||||
|
"ReceitaCandidatoFolder": "data/receitas_candidato"
|
||||||
|
},
|
||||||
|
"ParserSettings": {
|
||||||
|
"DefaultThreads": 40,
|
||||||
|
"CandidatoCSVThreads": 40,
|
||||||
|
"DepesasCSVThreads": 50,
|
||||||
|
"ReceitasCSVThreads": 50
|
||||||
},
|
},
|
||||||
"BasePath": "sample"
|
"BasePath": "sample"
|
||||||
}
|
}
|
||||||
|
87
README.md
87
README.md
@@ -9,3 +9,90 @@ OpenCand is built using:
|
|||||||
* .NET Core 8 - for the API
|
* .NET Core 8 - for the API
|
||||||
* PostgreSQL - for the database
|
* PostgreSQL - for the database
|
||||||
* React - for the front-end
|
* React - for the front-end
|
||||||
|
|
||||||
|
## Disponibilidade de dados (Prod)
|
||||||
|
|
||||||
|
* ✅ = Disponível
|
||||||
|
* ❌ = Não disponível
|
||||||
|
* ⛔ = Sem dados
|
||||||
|
|
||||||
|
| Nome do dado | Ano | Disponível? |
|
||||||
|
|-------------------|------|-------------|
|
||||||
|
| Candidatos | 2024 | ✅ |
|
||||||
|
| Bem Candidato | 2024 | ✅ |
|
||||||
|
| Despesas/Receitas | 2024 | ✅ |
|
||||||
|
| Rede Social | 2024 | ✅ |
|
||||||
|
| Fotos | 2024 | ✅ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2022 | ✅ |
|
||||||
|
| Bem Candidato | 2022 | ✅ |
|
||||||
|
| Despesas/Receitas | 2022 | ✅ |
|
||||||
|
| Rede Social | 2022 | ✅ |
|
||||||
|
| Fotos | 2022 | ✅ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2020 | ✅ |
|
||||||
|
| Bem Candidato | 2020 | ✅ |
|
||||||
|
| Despesas/Receitas | 2020 | ✅ |
|
||||||
|
| Rede Social | 2020 | ✅ |
|
||||||
|
| Fotos | 2020 | ❌ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2018 | ✅ |
|
||||||
|
| Bem Candidato | 2018 | ✅ |
|
||||||
|
| Despesas/Receitas | 2018 | ❌ |
|
||||||
|
| Rede Social | 2018 | ❌ |
|
||||||
|
| Fotos | 2018 | ❌ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2016 | ✅ |
|
||||||
|
| Bem Candidato | 2016 | ✅ |
|
||||||
|
| Despesas/Receitas | 2016 | ❌ |
|
||||||
|
| Rede Social | 2016 | ❌ |
|
||||||
|
| Fotos | 2016 | ❌ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2014 | ✅ |
|
||||||
|
| Bem Candidato | 2014 | ✅ |
|
||||||
|
| Despesas/Receitas | 2014 | ❌ |
|
||||||
|
| Rede Social | 2014 | ❌ |
|
||||||
|
| Fotos | 2014 | ❌ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2012 | ✅ |
|
||||||
|
| Bem Candidato | 2012 | ✅ |
|
||||||
|
| Despesas/Receitas | 2012 | ❌ |
|
||||||
|
| Rede Social | 2012 | ❌ |
|
||||||
|
| Fotos | 2012 | ❌ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2010 | ✅ |
|
||||||
|
| Bem Candidato | 2010 | ✅ |
|
||||||
|
| Despesas/Receitas | 2010 | ❌ |
|
||||||
|
| Rede Social | 2010 | ❌ |
|
||||||
|
| Fotos | 2010 | ❌ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2008 | ✅ |
|
||||||
|
| Bem Candidato | 2008 | ✅ |
|
||||||
|
| Despesas/Receitas | 2008 | ❌ |
|
||||||
|
| Rede Social | 2008 | ❌ |
|
||||||
|
| Fotos | 2008 | ❌ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2006 | ✅ |
|
||||||
|
| Bem Candidato | 2006 | ✅ |
|
||||||
|
| Despesas/Receitas | 2006 | ❌ |
|
||||||
|
| Rede Social | 2006 | ❌ |
|
||||||
|
| Fotos | 2006 | ❌ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2004 | ✅ |
|
||||||
|
| Bem Candidato | 2004 | ❌ |
|
||||||
|
| Despesas/Receitas | 2004 | ❌ |
|
||||||
|
| Rede Social | 2004 | ❌ |
|
||||||
|
| Fotos | 2004 | ❌ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2002 | ✅ |
|
||||||
|
| Bem Candidato | 2002 | ❌ |
|
||||||
|
| Despesas/Receitas | 2002 | ❌ |
|
||||||
|
| Rede Social | 2002 | ❌ |
|
||||||
|
| Fotos | 2002 | ❌ |
|
||||||
|
| - | - | - |
|
||||||
|
| Candidatos | 2000 | ✅ |
|
||||||
|
| Bem Candidato | 2000 | ⛔ |
|
||||||
|
| Despesas/Receitas | 2000 | ⛔ |
|
||||||
|
| Rede Social | 2000 | ⛔ |
|
||||||
|
| Fotos | 2000 | ⛔ |
|
||||||
|
| - | - | - |
|
||||||
|
96
db/db.sql
96
db/db.sql
@@ -1,31 +1,36 @@
|
|||||||
DROP TABLE IF EXISTS bem_candidato CASCADE;
|
DROP TABLE IF EXISTS bem_candidato CASCADE;
|
||||||
DROP TABLE IF EXISTS candidato_mapping 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 rede_social CASCADE;
|
||||||
DROP TABLE IF EXISTS candidato CASCADE;
|
DROP TABLE IF EXISTS candidato CASCADE;
|
||||||
DROP TABLE IF EXISTS partido CASCADE;
|
DROP TABLE IF EXISTS partido CASCADE;
|
||||||
|
DROP TABLE IF EXISTS despesas_candidato CASCADE;
|
||||||
|
DROP TABLE IF EXISTS receitas_candidato CASCADE;
|
||||||
|
|
||||||
CREATE TABLE candidato (
|
CREATE TABLE candidato (
|
||||||
idcandidato UUID NOT NULL PRIMARY KEY,
|
idcandidato UUID NOT NULL PRIMARY KEY,
|
||||||
cpf VARCHAR(11),
|
cpf VARCHAR(11),
|
||||||
nome VARCHAR(255) NOT NULL,
|
nome VARCHAR(255) NOT NULL,
|
||||||
apelido VARCHAR(255),
|
|
||||||
datanascimento TIMESTAMPTZ,
|
datanascimento TIMESTAMPTZ,
|
||||||
email TEXT,
|
|
||||||
sexo CHAR(15),
|
sexo CHAR(15),
|
||||||
estadocivil VARCHAR(50),
|
apelido VARCHAR(255),
|
||||||
escolaridade VARCHAR(50),
|
localidade VARCHAR(100),
|
||||||
ocupacao VARCHAR(150)
|
ultimoano INT,
|
||||||
|
popularidade BIGINT DEFAULT 0
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_candidato_nome ON candidato (nome);
|
CREATE INDEX idx_candidato_nome ON candidato (nome);
|
||||||
CREATE INDEX idx_candidato_apelido ON candidato (apelido);
|
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)
|
-- Each candidato (idcandidato, cpf, nome) will be mapped to a (sqcandidato, ano, tipo_eleicao, sg_uf, cargo, resultado)
|
||||||
CREATE TABLE candidato_mapping (
|
CREATE TABLE candidato_mapping (
|
||||||
idcandidato UUID NOT NULL,
|
idcandidato UUID NOT NULL,
|
||||||
cpf VARCHAR(11),
|
cpf VARCHAR(11),
|
||||||
nome VARCHAR(255) NOT NULL,
|
nome VARCHAR(255) NOT NULL,
|
||||||
apelido VARCHAR(255),
|
sqcandidato VARCHAR(50) NOT NULL,
|
||||||
sqcandidato TEXT,
|
turno VARCHAR(2) NOT NULL,
|
||||||
ano INT NOT NULL,
|
ano INT NOT NULL,
|
||||||
tipoeleicao VARCHAR(50),
|
tipoeleicao VARCHAR(50),
|
||||||
siglauf VARCHAR(2),
|
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 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
|
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_cpf ON candidato_mapping (cpf);
|
||||||
CREATE INDEX idx_candidato_mapping_nome ON candidato_mapping (nome);
|
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_ano ON candidato_mapping (ano);
|
||||||
CREATE INDEX idx_candidato_mapping_sqcandidato ON candidato_mapping (sqcandidato);
|
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
|
---- Table for storing assets of candidates
|
||||||
CREATE TABLE bem_candidato (
|
CREATE TABLE bem_candidato (
|
||||||
idcandidato UUID NOT NULL,
|
idcandidato UUID NOT NULL,
|
||||||
@@ -77,3 +95,65 @@ CREATE TABLE partido (
|
|||||||
);
|
);
|
||||||
CREATE INDEX idx_partido_nome ON partido (nome);
|
CREATE INDEX idx_partido_nome ON partido (nome);
|
||||||
CREATE INDEX idx_partido_numero ON partido (numero);
|
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