diff --git a/OpenCand.API/Repository/CandidatoRepository.cs b/OpenCand.API/Repository/CandidatoRepository.cs index 7019e30..fb838ca 100644 --- a/OpenCand.API/Repository/CandidatoRepository.cs +++ b/OpenCand.API/Repository/CandidatoRepository.cs @@ -58,6 +58,17 @@ namespace OpenCand.Repository } } + public async Task GetPartidoBySigla(string sigla) + { + using (var connection = new NpgsqlConnection(ConnectionString)) + { + var query = @" + SELECT * FROM partido + WHERE sigla = @sigla"; + return await connection.QueryFirstOrDefaultAsync(query, new { sigla }); + } + } + public async Task?> GetCandidatoRedeSocialById(Guid idcandidato) { using (var connection = new NpgsqlConnection(ConnectionString)) diff --git a/OpenCand.API/Repository/OpenCandRepository.cs b/OpenCand.API/Repository/OpenCandRepository.cs index 3076352..1a7d3bb 100644 --- a/OpenCand.API/Repository/OpenCandRepository.cs +++ b/OpenCand.API/Repository/OpenCandRepository.cs @@ -21,7 +21,7 @@ namespace OpenCand.API.Repository (SELECT COUNT(*) FROM bem_candidato) AS TotalBemCandidatos, (SELECT SUM(valor) FROM bem_candidato) AS TotalValorBemCandidatos, (SELECT COUNT(*) FROM rede_social) AS TotalRedesSociais, - (SELECT COUNT(DISTINCT ano) FROM bem_candidato) AS TotalEleicoes;"); + (SELECT COUNT(DISTINCT ano) FROM candidato_mapping) AS TotalEleicoes;"); return stats ?? new OpenCandStats(); } } diff --git a/OpenCand.API/Services/OpenCandService.cs b/OpenCand.API/Services/OpenCandService.cs index 2e61a9d..eafdb68 100644 --- a/OpenCand.API/Services/OpenCandService.cs +++ b/OpenCand.API/Services/OpenCandService.cs @@ -50,6 +50,11 @@ namespace OpenCand.API.Services var result = await candidatoRepository.GetCandidatoAsync(idcandidato); var eleicoes = await candidatoRepository.GetCandidatoMappingById(idcandidato); + foreach (var eleicao in eleicoes) + { + eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido); + } + if (result == null) { throw new KeyNotFoundException($"Candidato with ID {idcandidato} not found."); diff --git a/OpenCand.Core/Models/Candidato.cs b/OpenCand.Core/Models/Candidato.cs index d4af8ba..5f91d5e 100644 --- a/OpenCand.Core/Models/Candidato.cs +++ b/OpenCand.Core/Models/Candidato.cs @@ -1,3 +1,5 @@ +using System.IO.Pipes; + namespace OpenCand.Core.Models { public class Candidato @@ -41,6 +43,9 @@ namespace OpenCand.Core.Models public string Cargo { get; set; } public string NrCandidato { get; set; } public string Resultado { get; set; } + public string Sgpartido { get; set; } + + public Partido? Partido { get; set; } // Nullable to allow for candidates without a party } public class RedeSocial diff --git a/OpenCand.Core/Models/Partido.cs b/OpenCand.Core/Models/Partido.cs new file mode 100644 index 0000000..515a53c --- /dev/null +++ b/OpenCand.Core/Models/Partido.cs @@ -0,0 +1,9 @@ +namespace OpenCand.Core.Models +{ + public class Partido + { + public string Sigla { get; set; } + public string Nome { get; set; } + public int Numero { get; set; } + } +} diff --git a/OpenCand.ETL/OpenCand.ETL.csproj b/OpenCand.ETL/OpenCand.ETL.csproj index 18d19bf..2af4f60 100644 --- a/OpenCand.ETL/OpenCand.ETL.csproj +++ b/OpenCand.ETL/OpenCand.ETL.csproj @@ -9,6 +9,7 @@ + diff --git a/OpenCand.ETL/Parser/Models/BemCandidatoCSV.cs b/OpenCand.ETL/Parser/Models/BemCandidatoCSV.cs index 8e60d7c..d55e818 100644 --- a/OpenCand.ETL/Parser/Models/BemCandidatoCSV.cs +++ b/OpenCand.ETL/Parser/Models/BemCandidatoCSV.cs @@ -4,30 +4,9 @@ namespace OpenCand.Parser.Models { public class BemCandidatoCSV { - [Name("DT_GERACAO")] - public string DataGeracao { get; set; } - - [Name("HH_GERACAO")] - public string HoraGeracao { get; set; } - [Name("ANO_ELEICAO")] public int AnoEleicao { get; set; } - [Name("CD_TIPO_ELEICAO")] - public int CodigoTipoEleicao { get; set; } - - [Name("NM_TIPO_ELEICAO")] - public string NomeTipoEleicao { get; set; } - - [Name("CD_ELEICAO")] - public int CodigoEleicao { get; set; } - - [Name("DS_ELEICAO")] - public string DescricaoEleicao { get; set; } - - [Name("DT_ELEICAO")] - public string DataEleicao { get; set; } - [Name("SG_UF")] public string SiglaUF { get; set; } @@ -40,12 +19,9 @@ namespace OpenCand.Parser.Models [Name("SQ_CANDIDATO")] public string SequencialCandidato { get; set; } - [Name("NR_ORDEM_BEM_CANDIDATO")] + [Name("NR_ORDEM_BEM_CANDIDATO", "NR_ORDEM_CANDIDATO")] public int NumeroOrdemBemCandidato { get; set; } - [Name("CD_TIPO_BEM_CANDIDATO")] - public int CodigoTipoBemCandidato { get; set; } - [Name("DS_TIPO_BEM_CANDIDATO")] public string DescricaoTipoBemCandidato { get; set; } diff --git a/OpenCand.ETL/Parser/Models/CandidatoCSV.cs b/OpenCand.ETL/Parser/Models/CandidatoCSV.cs index 6a2acd4..61cc632 100644 --- a/OpenCand.ETL/Parser/Models/CandidatoCSV.cs +++ b/OpenCand.ETL/Parser/Models/CandidatoCSV.cs @@ -5,48 +5,18 @@ namespace OpenCand.Parser.Models { public class CandidatoCSV { - [Name("DT_GERACAO")] - public string DataGeracao { get; set; } - - [Name("HH_GERACAO")] - public string HoraGeracao { get; set; } - [Name("ANO_ELEICAO")] public int AnoEleicao { get; set; } - [Name("CD_TIPO_ELEICAO")] - public int CodigoTipoEleicao { get; set; } - - [Name("NM_TIPO_ELEICAO")] - public string NomeTipoEleicao { get; set; } - - [Name("NR_TURNO")] - public int NumeroTurno { get; set; } - - [Name("CD_ELEICAO")] - public int CodigoEleicao { get; set; } - - [Name("DS_ELEICAO")] - public string DescricaoEleicao { get; set; } - - [Name("DT_ELEICAO")] - public string DataEleicao { get; set; } - [Name("TP_ABRANGENCIA")] public string TipoAbrangencia { get; set; } [Name("SG_UF")] public string SiglaUF { get; set; } - [Name("SG_UE")] - public string SiglaUE { get; set; } - [Name("NM_UE")] public string NomeUE { get; set; } - [Name("CD_CARGO")] - public int CodigoCargo { get; set; } - [Name("DS_CARGO")] public string DescricaoCargo { get; set; } @@ -59,21 +29,12 @@ namespace OpenCand.Parser.Models [Name("NM_CANDIDATO")] public string NomeCandidato { get; set; } - [Name("NM_URNA_CANDIDATO")] - public string NomeUrnaCandidato { get; set; } - - [Name("NM_SOCIAL_CANDIDATO")] - public string NomeSocialCandidato { get; set; } - [Name("NR_CPF_CANDIDATO")] public string CPFCandidato { get; set; } [Name("DS_EMAIL", "NM_EMAIL")] public string Email { get; set; } - [Name("SG_UF_NASCIMENTO")] - public string SiglaUFNascimento { get; set; } - [Name("DT_NASCIMENTO")] public string DataNascimento { get; set; } @@ -91,5 +52,14 @@ namespace OpenCand.Parser.Models [Name("DS_SIT_TOT_TURNO")] public string SituacaoTurno { get; set; } + + [Name("NR_PARTIDO")] + public int NumeroPartido { get; set; } + + [Name("SG_PARTIDO")] + public string SiglaPartido { get; set; } + + [Name("NM_PARTIDO")] + public string NomePartido { get; set; } } } diff --git a/OpenCand.ETL/Parser/Services/CsvParserService.cs b/OpenCand.ETL/Parser/Services/CsvParserService.cs index 47ddd8b..7e3aeea 100644 --- a/OpenCand.ETL/Parser/Services/CsvParserService.cs +++ b/OpenCand.ETL/Parser/Services/CsvParserService.cs @@ -63,7 +63,7 @@ namespace OpenCand.Parser.Services using var csv = new CsvReader(reader, parserConfig); var po = new ParallelOptions { - MaxDegreeOfParallelism = 100 + MaxDegreeOfParallelism = 25 }; csv.Context.RegisterClassMap(); @@ -111,6 +111,12 @@ namespace OpenCand.Parser.Services Cargo = record.DescricaoCargo, NrCandidato = record.NumeroCandidato, Resultado = record.SituacaoTurno, + Partido = new Partido + { + Sigla = record.SiglaPartido, + Nome = record.NomePartido, + Numero = record.NumeroPartido, + } } } }; diff --git a/OpenCand.ETL/Program.cs b/OpenCand.ETL/Program.cs index 33397c3..7a5317c 100644 --- a/OpenCand.ETL/Program.cs +++ b/OpenCand.ETL/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenCand.Config; +using OpenCand.ETL.Repository; using OpenCand.Parser; using OpenCand.Parser.Services; using OpenCand.Repository; @@ -64,6 +65,7 @@ namespace OpenCand services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); }); } diff --git a/OpenCand.ETL/Repository/CandidatoRepository.cs b/OpenCand.ETL/Repository/CandidatoRepository.cs index d573a4f..6a63d81 100644 --- a/OpenCand.ETL/Repository/CandidatoRepository.cs +++ b/OpenCand.ETL/Repository/CandidatoRepository.cs @@ -47,8 +47,8 @@ namespace OpenCand.Repository using (var connection = new NpgsqlConnection(ConnectionString)) { await connection.ExecuteAsync(@" - INSERT INTO candidato_mapping (idcandidato, cpf, nome, sqcandidato, ano, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, resultado) - VALUES (@idcandidato, @cpf, @nome, @sqcandidato, @ano, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @resultado);", + INSERT INTO candidato_mapping (idcandidato, cpf, nome, sqcandidato, ano, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado) + VALUES (@idcandidato, @cpf, @nome, @sqcandidato, @ano, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado);", new { idcandidato = candidatoMapping.IdCandidato, @@ -61,6 +61,7 @@ namespace OpenCand.Repository nomeue = candidatoMapping.NomeUE, nrcandidato = candidatoMapping.NrCandidato, cargo = candidatoMapping.Cargo, + sgpartido = candidatoMapping.Partido?.Sigla, resultado = candidatoMapping.Resultado }); } @@ -78,15 +79,15 @@ namespace OpenCand.Repository } } - public async Task?> GetCandidatoMappingByNome(string nome) + public async Task?> GetCandidatoMappingByNome(string nome, string sqcandidato, int ano, string siglauf, string nomeue, string nrcandidato) { using (var connection = new NpgsqlConnection(ConnectionString)) { var query = @" SELECT idcandidato, cpf, nome, sqcandidato, ano, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, resultado FROM candidato_mapping - WHERE nome = @nome"; - return (await connection.QueryAsync(query, new { nome })).AsList(); + WHERE nome = @nome AND sqcandidato = @sqcandidato AND ano = @ano AND siglauf = @siglauf AND nomeue = @nomeue AND nrcandidato = @nrcandidato"; + return (await connection.QueryAsync(query, new { nome, sqcandidato, ano, siglauf, nomeue, nrcandidato })).AsList(); } } diff --git a/OpenCand.ETL/Repository/PartidoRepository.cs b/OpenCand.ETL/Repository/PartidoRepository.cs new file mode 100644 index 0000000..5e145c0 --- /dev/null +++ b/OpenCand.ETL/Repository/PartidoRepository.cs @@ -0,0 +1,49 @@ +using Dapper; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using OpenCand.Core.Models; +using OpenCand.Repository; + +namespace OpenCand.ETL.Repository +{ + public class PartidoRepository : BaseRepository + { + // Memory cache for partido data + private readonly MemoryCache partidoCache = new MemoryCache(new MemoryCacheOptions()); + + public PartidoRepository(IConfiguration configuration) : base(configuration) + { + // Initialize the cache with a sliding expiration of 5 minutes + partidoCache = new MemoryCache(new MemoryCacheOptions + { + ExpirationScanFrequency = TimeSpan.FromMinutes(5) + }); + } + + public async Task AddPartidoAsync(Partido partido) + { + // Check if partido is already cached + if (partidoCache.TryGetValue(partido.Sigla, out Partido? cachedPartido)) + { + // If partido is already cached, no need to insert again + return; + } + + using (var connection = new Npgsql.NpgsqlConnection(ConnectionString)) + { + await connection.ExecuteAsync(@" + INSERT INTO partido (sigla, nome, numero) + VALUES (@sigla, @nome, @numero) + ON CONFLICT DO NOTHING;", + new + { + sigla = partido.Sigla, + nome = partido.Nome, + numero = partido.Numero + }); + + partidoCache.Set(partido.Sigla, partido, TimeSpan.FromMinutes(5)); + } + } + } +} diff --git a/OpenCand.ETL/Services/CandidatoService.cs b/OpenCand.ETL/Services/CandidatoService.cs index f23ec39..5f332ee 100644 --- a/OpenCand.ETL/Services/CandidatoService.cs +++ b/OpenCand.ETL/Services/CandidatoService.cs @@ -1,4 +1,5 @@ using OpenCand.Core.Models; +using OpenCand.ETL.Repository; using OpenCand.Repository; namespace OpenCand.Services @@ -6,10 +7,12 @@ namespace OpenCand.Services public class CandidatoService { private readonly CandidatoRepository candidatoRepository; + private readonly PartidoRepository partidoRepository; - public CandidatoService(CandidatoRepository candidatoRepository) + public CandidatoService(CandidatoRepository candidatoRepository, PartidoRepository partidoRepository) { this.candidatoRepository = candidatoRepository; + this.partidoRepository = partidoRepository; } public async Task AddCandidatoAsync(Candidato candidato) @@ -26,11 +29,23 @@ namespace OpenCand.Services var candidatoMapping = candidato.Eleicoes.First(); + // Add partido data + if (candidatoMapping.Partido != null) + { + await partidoRepository.AddPartidoAsync(candidatoMapping.Partido); + } + List? mappings = null; CandidatoMapping? existingMapping = null; if (candidato.Cpf == null || candidato.Cpf.Length != 11) { - mappings = await candidatoRepository.GetCandidatoMappingByNome(candidato.Nome); + // If CPF is not provided or invalid, we STRICTLY search by name and other properties + mappings = await candidatoRepository.GetCandidatoMappingByNome(candidato.Nome, + candidato.SqCandidato, + candidatoMapping.Ano, + candidatoMapping.SiglaUF, + candidatoMapping.NomeUE, + candidatoMapping.NrCandidato); } else { diff --git a/db/db.sql b/db/db.sql index 02d063b..d23ca6e 100644 --- a/db/db.sql +++ b/db/db.sql @@ -2,6 +2,7 @@ DROP TABLE IF EXISTS bem_candidato CASCADE; DROP TABLE IF EXISTS candidato_mapping CASCADE; DROP TABLE IF EXISTS rede_social CASCADE; DROP TABLE IF EXISTS candidato CASCADE; +DROP TABLE IF EXISTS partido CASCADE; CREATE TABLE candidato ( idcandidato UUID NOT NULL PRIMARY KEY, @@ -27,6 +28,7 @@ CREATE TABLE candidato_mapping ( siglauf VARCHAR(2), nomeue VARCHAR(100), cargo VARCHAR(50), + sgpartido VARCHAR(10), nrcandidato VARCHAR(20), resultado VARCHAR(50), CONSTRAINT pk_candidato_mapping PRIMARY KEY (idcandidato, ano, siglauf, nomeue, cargo, nrcandidato, resultado), @@ -37,6 +39,7 @@ CREATE INDEX idx_candidato_mapping_nome ON candidato_mapping (nome); CREATE INDEX idx_candidato_mapping_ano ON candidato_mapping (ano); CREATE INDEX idx_candidato_mapping_sqcandidato ON candidato_mapping (sqcandidato); +---- Table for storing assets of candidates CREATE TABLE bem_candidato ( idcandidato UUID NOT NULL, ano INT NOT NULL, @@ -50,6 +53,7 @@ ALTER TABLE bem_candidato ADD CONSTRAINT pk_bem_candidato PRIMARY KEY (idcandida CREATE INDEX idx_bem_candidato_idcandidato ON bem_candidato (idcandidato); CREATE INDEX idx_bem_candidato_valor ON bem_candidato (valor); +---- Table for storing social media links of candidates CREATE TABLE rede_social ( idcandidato UUID NOT NULL, rede VARCHAR(50) NOT NULL, @@ -59,4 +63,13 @@ CREATE TABLE rede_social ( CONSTRAINT pk_rede_social PRIMARY KEY (idcandidato, rede, siglauf, ano), CONSTRAINT fk_rede_social_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE ); -CREATE INDEX idx_rede_social_idcandidato ON rede_social (idcandidato); \ No newline at end of file +CREATE INDEX idx_rede_social_idcandidato ON rede_social (idcandidato); + +---- Table for storing party information +CREATE TABLE partido ( + sigla VARCHAR(10) NOT NULL PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + numero INT NOT NULL +); +CREATE INDEX idx_partido_nome ON partido (nome); +CREATE INDEX idx_partido_numero ON partido (numero); \ No newline at end of file