Retry Con Cambios Importantes.
This commit is contained in:
@@ -14,7 +14,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+69ddf2b2d24d4968c618c6fd9f38c1143625cdcd")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["mhE0FuBM0BOF9SNOE0rY9setCw2ye3UUh7cEPjhgDdY=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","jLJZbvuuUsGPkZf3QtwRFQTqJiXdNIIW3av7i2nQ\u002B30=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","BwAWLB3mTJ9blXh5ZTZOjcrdnzPEd9wZsoNfmceZfb8="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["mhE0FuBM0BOF9SNOE0rY9setCw2ye3UUh7cEPjhgDdY=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","7rMeSoKKF2\u002B9j5kLZ30FlE98meJ1tr4dywVzhYb49Qg="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
@@ -1 +1 @@
|
||||
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["mhE0FuBM0BOF9SNOE0rY9setCw2ye3UUh7cEPjhgDdY=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","jLJZbvuuUsGPkZf3QtwRFQTqJiXdNIIW3av7i2nQ\u002B30=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","BwAWLB3mTJ9blXh5ZTZOjcrdnzPEd9wZsoNfmceZfb8="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["mhE0FuBM0BOF9SNOE0rY9setCw2ye3UUh7cEPjhgDdY=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","7rMeSoKKF2\u002B9j5kLZ30FlE98meJ1tr4dywVzhYb49Qg="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+69ddf2b2d24d4968c618c6fd9f38c1143625cdcd")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -18,26 +18,37 @@ public class EleccionesDbContext(DbContextOptions<EleccionesDbContext> options)
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder); // Es buena práctica llamar a la base
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Configuraciones adicionales del modelo (índices, etc.) pueden ir aquí.
|
||||
// Por ejemplo, para optimizar las búsquedas.
|
||||
modelBuilder.Entity<ResultadoVoto>()
|
||||
.HasIndex(r => new { r.AmbitoGeograficoId, r.AgrupacionPoliticaId })
|
||||
.IsUnique();
|
||||
|
||||
// Precisión para los campos de porcentaje en EstadoRecuento
|
||||
modelBuilder.Entity<EstadoRecuento>(entity =>
|
||||
{
|
||||
entity.Property(e => e.MesasTotalizadasPorcentaje).HasPrecision(5, 2);
|
||||
entity.Property(e => e.ParticipacionPorcentaje).HasPrecision(5, 2);
|
||||
entity.Property(e => e.VotosNulosPorcentaje).HasPrecision(18, 4);
|
||||
entity.Property(e => e.VotosEnBlancoPorcentaje).HasPrecision(18, 4);
|
||||
entity.Property(e => e.VotosRecurridosPorcentaje).HasPrecision(18, 4);
|
||||
});
|
||||
|
||||
// Precisión para el campo de porcentaje en ResultadoVoto
|
||||
modelBuilder.Entity<ResultadoVoto>()
|
||||
.Property(e => e.PorcentajeVotos).HasPrecision(18, 4);
|
||||
|
||||
modelBuilder.Entity<ResumenVoto>()
|
||||
.Property(e => e.VotosPorcentaje).HasPrecision(5, 2);
|
||||
.Property(e => e.VotosPorcentaje).HasPrecision(5, 2);
|
||||
|
||||
modelBuilder.Entity<EstadoRecuentoGeneral>(entity =>
|
||||
{
|
||||
entity.Property(e => e.MesasTotalizadasPorcentaje).HasPrecision(5, 2);
|
||||
entity.Property(e => e.ParticipacionPorcentaje).HasPrecision(5, 2);
|
||||
});
|
||||
{
|
||||
// Le decimos a EF que la combinación única es (AmbitoGeograficoId, CategoriaId)
|
||||
entity.HasKey(e => new { e.AmbitoGeograficoId, e.CategoriaId });
|
||||
|
||||
// Mantener la configuración de precisión
|
||||
entity.Property(e => e.MesasTotalizadasPorcentaje).HasPrecision(5, 2);
|
||||
entity.Property(e => e.ParticipacionPorcentaje).HasPrecision(5, 2);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// src/Elecciones.Database/Entities/EstadoRecuentoGeneral.cs
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
@@ -5,14 +6,16 @@ namespace Elecciones.Database.Entities;
|
||||
|
||||
public class EstadoRecuentoGeneral
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.None)] // Le dice a EF que no genere este valor.
|
||||
public int AmbitoGeograficoId { get; set; }
|
||||
|
||||
public int CategoriaId { get; set; }
|
||||
public int MesasEsperadas { get; set; }
|
||||
public int MesasTotalizadas { get; set; }
|
||||
public decimal MesasTotalizadasPorcentaje { get; set; }
|
||||
public int CantidadElectores { get; set; }
|
||||
public int CantidadVotantes { get; set; }
|
||||
public decimal ParticipacionPorcentaje { get; set; }
|
||||
|
||||
// --- Propiedades de navegación (Opcional pero recomendado) ---
|
||||
[ForeignKey("CategoriaId")]
|
||||
public CategoriaElectoral CategoriaElectoral { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Elecciones.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Elecciones.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(EleccionesDbContext))]
|
||||
[Migration("20250817230412_MakeEstadoGeneralKeyComposite")]
|
||||
partial class MakeEstadoGeneralKeyComposite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.AgrupacionPolitica", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("IdTelegrama")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Nombre")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AgrupacionesPoliticas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.AmbitoGeografico", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CircuitoId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DistritoId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("EstablecimientoId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("MesaId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("MunicipioId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("NivelId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Nombre")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SeccionId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SeccionProvincialId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AmbitosGeograficos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.CategoriaElectoral", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Nombre")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Orden")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("CategoriasElectorales");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuento", b =>
|
||||
{
|
||||
b.Property<int>("AmbitoGeograficoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("CantidadElectores")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("CantidadVotantes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("FechaTotalizacion")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("MesasEsperadas")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("MesasTotalizadas")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("MesasTotalizadasPorcentaje")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.Property<decimal>("ParticipacionPorcentaje")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.Property<long>("VotosEnBlanco")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("VotosEnBlancoPorcentaje")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<long>("VotosNulos")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("VotosNulosPorcentaje")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<long>("VotosRecurridos")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("VotosRecurridosPorcentaje")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.HasKey("AmbitoGeograficoId");
|
||||
|
||||
b.ToTable("EstadosRecuentos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b =>
|
||||
{
|
||||
b.Property<int>("AmbitoGeograficoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("CategoriaId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("CantidadElectores")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("CantidadVotantes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("MesasEsperadas")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("MesasTotalizadas")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("MesasTotalizadasPorcentaje")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.Property<decimal>("ParticipacionPorcentaje")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.HasKey("AmbitoGeograficoId", "CategoriaId");
|
||||
|
||||
b.HasIndex("CategoriaId");
|
||||
|
||||
b.ToTable("EstadosRecuentosGenerales");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AgrupacionPoliticaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AmbitoGeograficoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("NroBancas")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AgrupacionPoliticaId");
|
||||
|
||||
b.HasIndex("AmbitoGeograficoId");
|
||||
|
||||
b.ToTable("ProyeccionesBancas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.ResultadoVoto", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("AgrupacionPoliticaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AmbitoGeograficoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("CantidadVotos")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("PorcentajeVotos")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AgrupacionPoliticaId");
|
||||
|
||||
b.HasIndex("AmbitoGeograficoId", "AgrupacionPoliticaId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ResultadosVotos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.ResumenVoto", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AgrupacionPoliticaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("AmbitoGeograficoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("Votos")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("VotosPorcentaje")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ResumenesVotos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.Telegrama", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AmbitoGeograficoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContenidoBase64")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("FechaEscaneo")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("FechaTotalizacion")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Telegramas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuento", b =>
|
||||
{
|
||||
b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico")
|
||||
.WithMany()
|
||||
.HasForeignKey("AmbitoGeograficoId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AmbitoGeografico");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b =>
|
||||
{
|
||||
b.HasOne("Elecciones.Database.Entities.CategoriaElectoral", "CategoriaElectoral")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoriaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CategoriaElectoral");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b =>
|
||||
{
|
||||
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
|
||||
.WithMany()
|
||||
.HasForeignKey("AgrupacionPoliticaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico")
|
||||
.WithMany()
|
||||
.HasForeignKey("AmbitoGeograficoId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AgrupacionPolitica");
|
||||
|
||||
b.Navigation("AmbitoGeografico");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.ResultadoVoto", b =>
|
||||
{
|
||||
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
|
||||
.WithMany()
|
||||
.HasForeignKey("AgrupacionPoliticaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico")
|
||||
.WithMany()
|
||||
.HasForeignKey("AmbitoGeograficoId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AgrupacionPolitica");
|
||||
|
||||
b.Navigation("AmbitoGeografico");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Elecciones.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MakeEstadoGeneralKeyComposite : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_EstadosRecuentosGenerales",
|
||||
table: "EstadosRecuentosGenerales");
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "PorcentajeVotos",
|
||||
table: "ResultadosVotos",
|
||||
type: "decimal(18,4)",
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: false,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "decimal(18,2)");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CategoriaId",
|
||||
table: "EstadosRecuentosGenerales",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "VotosRecurridosPorcentaje",
|
||||
table: "EstadosRecuentos",
|
||||
type: "decimal(18,4)",
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: false,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "decimal(18,2)");
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "VotosNulosPorcentaje",
|
||||
table: "EstadosRecuentos",
|
||||
type: "decimal(18,4)",
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: false,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "decimal(18,2)");
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "VotosEnBlancoPorcentaje",
|
||||
table: "EstadosRecuentos",
|
||||
type: "decimal(18,4)",
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: false,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "decimal(18,2)");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_EstadosRecuentosGenerales",
|
||||
table: "EstadosRecuentosGenerales",
|
||||
columns: new[] { "AmbitoGeograficoId", "CategoriaId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EstadosRecuentosGenerales_CategoriaId",
|
||||
table: "EstadosRecuentosGenerales",
|
||||
column: "CategoriaId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_EstadosRecuentosGenerales_CategoriasElectorales_CategoriaId",
|
||||
table: "EstadosRecuentosGenerales",
|
||||
column: "CategoriaId",
|
||||
principalTable: "CategoriasElectorales",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_EstadosRecuentosGenerales_CategoriasElectorales_CategoriaId",
|
||||
table: "EstadosRecuentosGenerales");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_EstadosRecuentosGenerales",
|
||||
table: "EstadosRecuentosGenerales");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EstadosRecuentosGenerales_CategoriaId",
|
||||
table: "EstadosRecuentosGenerales");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CategoriaId",
|
||||
table: "EstadosRecuentosGenerales");
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "PorcentajeVotos",
|
||||
table: "ResultadosVotos",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "decimal(18,4)",
|
||||
oldPrecision: 18,
|
||||
oldScale: 4);
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "VotosRecurridosPorcentaje",
|
||||
table: "EstadosRecuentos",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "decimal(18,4)",
|
||||
oldPrecision: 18,
|
||||
oldScale: 4);
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "VotosNulosPorcentaje",
|
||||
table: "EstadosRecuentos",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "decimal(18,4)",
|
||||
oldPrecision: 18,
|
||||
oldScale: 4);
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "VotosEnBlancoPorcentaje",
|
||||
table: "EstadosRecuentos",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "decimal(18,4)",
|
||||
oldPrecision: 18,
|
||||
oldScale: 4);
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_EstadosRecuentosGenerales",
|
||||
table: "EstadosRecuentosGenerales",
|
||||
column: "AmbitoGeograficoId");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,19 +130,22 @@ namespace Elecciones.Database.Migrations
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("VotosEnBlancoPorcentaje")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<long>("VotosNulos")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("VotosNulosPorcentaje")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<long>("VotosRecurridos")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("VotosRecurridosPorcentaje")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.HasKey("AmbitoGeograficoId");
|
||||
|
||||
@@ -154,6 +157,9 @@ namespace Elecciones.Database.Migrations
|
||||
b.Property<int>("AmbitoGeograficoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("CategoriaId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("CantidadElectores")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -174,7 +180,9 @@ namespace Elecciones.Database.Migrations
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.HasKey("AmbitoGeograficoId");
|
||||
b.HasKey("AmbitoGeograficoId", "CategoriaId");
|
||||
|
||||
b.HasIndex("CategoriaId");
|
||||
|
||||
b.ToTable("EstadosRecuentosGenerales");
|
||||
});
|
||||
@@ -225,7 +233,8 @@ namespace Elecciones.Database.Migrations
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("PorcentajeVotos")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -298,6 +307,17 @@ namespace Elecciones.Database.Migrations
|
||||
b.Navigation("AmbitoGeografico");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b =>
|
||||
{
|
||||
b.HasOne("Elecciones.Database.Entities.CategoriaElectoral", "CategoriaElectoral")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoriaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CategoriaElectoral");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b =>
|
||||
{
|
||||
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
|
||||
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+69ddf2b2d24d4968c618c6fd9f38c1143625cdcd")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -53,20 +53,30 @@ public class ElectoralApiService : IElectoralApiService
|
||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<AgrupacionDto>>() : null;
|
||||
}
|
||||
|
||||
public async Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string seccionId, string municipioId)
|
||||
public async Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string seccionId, string? municipioId, int categoriaId)
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||
var requestUri = $"/api/resultados/getResultados?distritoId={distritoId}&seccionId={seccionId}&municipioId={municipioId}&categoriaId=5"; // OJO: La categoría está fija
|
||||
|
||||
// Construimos la URL base
|
||||
var requestUri = $"/api/resultados/getResultados?distritoId={distritoId}&seccionId={seccionId}&categoriaId={categoriaId}";
|
||||
|
||||
// Añadimos el municipioId a la URL SÓLO si no es nulo o vacío
|
||||
if (!string.IsNullOrEmpty(municipioId))
|
||||
{
|
||||
requestUri += $"&municipioId={municipioId}";
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||
request.Headers.Add("Authorization", $"Bearer {authToken}");
|
||||
var response = await client.SendAsync(request);
|
||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<ResultadosDto>() : null;
|
||||
}
|
||||
|
||||
public async Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string seccionId)
|
||||
public async Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string seccionId, int categoriaId)
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||
var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&seccionId={seccionId}&categoriaId=5"; // OJO: La categoría está fija
|
||||
// Usamos la categoriaId recibida en lugar de una fija
|
||||
var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&seccionId={seccionId}&categoriaId={categoriaId}";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||
request.Headers.Add("Authorization", $"Bearer {authToken}");
|
||||
var response = await client.SendAsync(request);
|
||||
@@ -103,10 +113,11 @@ public class ElectoralApiService : IElectoralApiService
|
||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<ResumenDto>() : null;
|
||||
}
|
||||
|
||||
public async Task<EstadoRecuentoGeneralDto?> GetEstadoRecuentoGeneralAsync(string authToken, string distritoId)
|
||||
public async Task<EstadoRecuentoGeneralDto?> GetEstadoRecuentoGeneralAsync(string authToken, string distritoId, int categoriaId)
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||
var requestUri = $"/api/estados/estadoRecuento?distritoId={distritoId}";
|
||||
// La URL ahora usa el parámetro 'categoriaId' que se recibe
|
||||
var requestUri = $"/api/estados/estadoRecuento?distritoId={distritoId}&categoriaId={categoriaId}";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||
request.Headers.Add("Authorization", $"Bearer {authToken}");
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
@@ -12,11 +12,11 @@ public interface IElectoralApiService
|
||||
// Métodos para catálogos
|
||||
Task<CatalogoDto?> GetCatalogoAmbitosAsync(string authToken, int categoriaId);
|
||||
Task<List<AgrupacionDto>?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId);
|
||||
Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string seccionId, string municipioId);
|
||||
Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string seccionId);
|
||||
Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string seccionId, string? municipioId, int categoriaId);
|
||||
Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string seccionId, int categoriaId);
|
||||
Task<List<string[]>?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId);
|
||||
Task<TelegramaFileDto?> GetTelegramaFileAsync(string authToken, string mesaId);
|
||||
Task<ResumenDto?> GetResumenAsync(string authToken, string distritoId);
|
||||
Task<EstadoRecuentoGeneralDto?> GetEstadoRecuentoGeneralAsync(string authToken, string distritoId);
|
||||
Task<EstadoRecuentoGeneralDto?> GetEstadoRecuentoGeneralAsync(string authToken, string distritoId, int categoriaId);
|
||||
Task<List<CategoriaDto>?> GetCategoriasAsync(string authToken);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+69ddf2b2d24d4968c618c6fd9f38c1143625cdcd")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -10,12 +10,15 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Elecciones.Worker;
|
||||
|
||||
/// <summary>
|
||||
/// Servicio de fondo (BackgroundService) responsable de sincronizar y sondear
|
||||
/// periódicamente los datos de la API electoral.
|
||||
/// </summary>
|
||||
public class Worker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<Worker> _logger;
|
||||
private readonly IElectoralApiService _apiService;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private string? _authToken; // Almacenamos el token para reutilizarlo en el ciclo de vida
|
||||
|
||||
public Worker(ILogger<Worker> logger, IElectoralApiService apiService, IServiceProvider serviceProvider)
|
||||
{
|
||||
@@ -24,154 +27,75 @@ public class Worker : BackgroundService
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Método principal del worker que se ejecuta en segundo plano.
|
||||
/// </summary>
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Elecciones Worker iniciado a las: {time}", DateTimeOffset.Now);
|
||||
|
||||
await SincronizarCatalogosAsync(stoppingToken);
|
||||
// 1. SINCRONIZACIÓN INICIAL: Se ejecuta una vez al iniciar el worker para
|
||||
// asegurar que la base de datos local tenga todos los catálogos maestros.
|
||||
await SincronizarCatalogosMaestrosAsync(stoppingToken);
|
||||
|
||||
_logger.LogInformation("-------------------------------------------------");
|
||||
_logger.LogInformation("Iniciando sondeo periódico de resultados...");
|
||||
_logger.LogInformation("-------------------------------------------------");
|
||||
|
||||
// 2. BUCLE DE SONDEO: Se ejecuta continuamente hasta que se detenga la aplicación.
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
// Ejecutamos todos los sondeos en paralelo para mayor eficiencia
|
||||
var authToken = await _apiService.GetAuthTokenAsync();
|
||||
if (string.IsNullOrEmpty(authToken))
|
||||
{
|
||||
_logger.LogError("CRÍTICO: No se pudo obtener el token de autenticación. Reintentando en 1 minuto...");
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
await Task.WhenAll(
|
||||
SondearResultadosAsync(stoppingToken),
|
||||
SondearBancasAsync(stoppingToken),
|
||||
SondearTelegramasAsync(stoppingToken),
|
||||
SondearResumenProvincialAsync(stoppingToken),
|
||||
SondearEstadoRecuentoGeneralAsync(stoppingToken)
|
||||
SondearResultadosMunicipalesAsync(authToken, stoppingToken),
|
||||
SondearProyeccionBancasAsync(authToken, stoppingToken),
|
||||
SondearNuevosTelegramasAsync(authToken, stoppingToken),
|
||||
SondearResumenProvincialAsync(authToken, stoppingToken),
|
||||
SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken)
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
// Esperamos 1 minuto antes del siguiente ciclo completo de sondeos
|
||||
_logger.LogInformation("Ciclo de sondeo completado. Esperando 5 minuto para el siguiente...");
|
||||
_logger.LogInformation("Ciclo de sondeo completado. Esperando 5 minutos para el siguiente...");
|
||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||
}
|
||||
catch (TaskCanceledException) { break; }
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Elecciones Worker se está deteniendo.");
|
||||
}
|
||||
|
||||
|
||||
private async Task ObtenerTokenSiEsNecesario(CancellationToken stoppingToken)
|
||||
{
|
||||
// En un futuro, se podría añadir lógica para renovar el token solo cuando expire.
|
||||
// Por ahora, para asegurar que siempre sea válido, lo obtenemos cada vez.
|
||||
_authToken = await _apiService.GetAuthTokenAsync();
|
||||
if (string.IsNullOrEmpty(_authToken))
|
||||
{
|
||||
_logger.LogError("No se pudo obtener el token, se cancelan las operaciones.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SondearResultadosAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ObtenerTokenSiEsNecesario(stoppingToken);
|
||||
if (string.IsNullOrEmpty(_authToken) || stoppingToken.IsCancellationRequested) return;
|
||||
|
||||
// Usamos un 'scope' propio para esta operación
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||
|
||||
// 1. OBTENEMOS LA LISTA DE TODOS LOS MUNICIPIOS DE NUESTRA BD
|
||||
var municipiosASondear = await dbContext.AmbitosGeograficos
|
||||
.AsNoTracking()
|
||||
.Where(a => a.NivelId == 5 && a.MunicipioId != null && a.DistritoId != null && a.SeccionId != null)
|
||||
.Select(a => new { a.Id, a.MunicipioId, a.SeccionId, a.DistritoId })
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
_logger.LogInformation("Iniciando sondeo para {count} municipios.", municipiosASondear.Count);
|
||||
|
||||
// 2. RECORREMOS LA LISTA Y PROCESAMOS CADA MUNICIPIO
|
||||
foreach (var municipio in municipiosASondear)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
_logger.LogInformation("Sondeando resultados para el municipio: {municipioId}", municipio.MunicipioId);
|
||||
|
||||
var resultados = await _apiService.GetResultadosAsync(_authToken, municipio.DistritoId!, municipio.SeccionId!, municipio.MunicipioId!);
|
||||
if (resultados is null)
|
||||
{
|
||||
_logger.LogWarning("No se recibieron resultados para el municipio {municipioId}", municipio.MunicipioId);
|
||||
continue; // Saltamos al siguiente municipio
|
||||
}
|
||||
|
||||
// 3. GUARDAMOS LOS DATOS (Lógica de Upsert)
|
||||
// La lógica de guardado que ya teníamos funciona perfectamente para cada municipio.
|
||||
// La pasamos a un método separado para mayor claridad.
|
||||
await GuardarResultadosDeAmbitoAsync(dbContext, municipio.Id, resultados, stoppingToken);
|
||||
|
||||
_logger.LogInformation("Resultados para el municipio {municipioId} guardados/actualizados.", municipio.MunicipioId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GuardarResultadosDeAmbitoAsync(EleccionesDbContext dbContext, int ambitoId, Elecciones.Core.DTOs.ResultadosDto resultados, CancellationToken stoppingToken)
|
||||
{
|
||||
// --- ACTUALIZAR O INSERTAR ESTADO RECUENTO ---
|
||||
var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId }, cancellationToken: stoppingToken);
|
||||
if (estadoRecuento == null)
|
||||
{
|
||||
estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId };
|
||||
dbContext.EstadosRecuentos.Add(estadoRecuento);
|
||||
}
|
||||
estadoRecuento.FechaTotalizacion = DateTime.Parse(resultados.FechaTotalizacion).ToUniversalTime();
|
||||
estadoRecuento.MesasEsperadas = resultados.EstadoRecuento.MesasEsperadas;
|
||||
estadoRecuento.MesasTotalizadas = resultados.EstadoRecuento.MesasTotalizadas;
|
||||
estadoRecuento.CantidadElectores = resultados.EstadoRecuento.CantidadElectores;
|
||||
estadoRecuento.ParticipacionPorcentaje = resultados.EstadoRecuento.ParticipacionPorcentaje;
|
||||
if (resultados.ValoresTotalizadosOtros != null)
|
||||
{
|
||||
estadoRecuento.VotosEnBlanco = resultados.ValoresTotalizadosOtros.VotosEnBlanco;
|
||||
estadoRecuento.VotosNulos = resultados.ValoresTotalizadosOtros.VotosNulos;
|
||||
estadoRecuento.VotosRecurridos = resultados.ValoresTotalizadosOtros.VotosRecurridos;
|
||||
}
|
||||
|
||||
// --- ACTUALIZAR O INSERTAR VOTOS POSITIVOS ---
|
||||
foreach (var votoPositivo in resultados.ValoresTotalizadosPositivos)
|
||||
{
|
||||
var resultadoVoto = await dbContext.ResultadosVotos.FirstOrDefaultAsync(
|
||||
rv => rv.AmbitoGeograficoId == ambitoId && rv.AgrupacionPoliticaId == votoPositivo.IdAgrupacion, stoppingToken);
|
||||
|
||||
if (resultadoVoto == null)
|
||||
{
|
||||
resultadoVoto = new ResultadoVoto
|
||||
{
|
||||
AmbitoGeograficoId = ambitoId,
|
||||
AgrupacionPoliticaId = votoPositivo.IdAgrupacion
|
||||
};
|
||||
dbContext.ResultadosVotos.Add(resultadoVoto);
|
||||
}
|
||||
resultadoVoto.CantidadVotos = votoPositivo.Votos;
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
}
|
||||
|
||||
private async Task SincronizarCatalogosAsync(CancellationToken stoppingToken)
|
||||
/// <summary>
|
||||
/// Descarga y sincroniza los catálogos base (Categorías, Ámbitos, Agrupaciones)
|
||||
/// desde la API a la base de datos local.
|
||||
/// </summary>
|
||||
private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Iniciando sincronización de catálogos maestros...");
|
||||
await ObtenerTokenSiEsNecesario(stoppingToken);
|
||||
if (string.IsNullOrEmpty(_authToken) || stoppingToken.IsCancellationRequested) return;
|
||||
var authToken = await _apiService.GetAuthTokenAsync();
|
||||
if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogError("No se pudo obtener token para la sincronización de catálogos.");
|
||||
return;
|
||||
}
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||
|
||||
// --- 1. SINCRONIZAR CATEGORÍAS ---
|
||||
var categoriasApi = await _apiService.GetCategoriasAsync(_authToken);
|
||||
// --- 1. SINCRONIZAR CATEGORÍAS ELECTORALES ---
|
||||
var categoriasApi = await _apiService.GetCategoriasAsync(authToken);
|
||||
if (categoriasApi is null || !categoriasApi.Any())
|
||||
{
|
||||
_logger.LogWarning("No se recibieron datos del catálogo de Categorías.");
|
||||
@@ -189,15 +113,17 @@ public class Worker : BackgroundService
|
||||
dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden });
|
||||
}
|
||||
}
|
||||
// Guardamos las categorías primero para asegurar que existan para los siguientes pasos
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
|
||||
// --- 2. SINCRONIZAR ÁMBITOS Y AGRUPACIONES POR CADA CATEGORÍA ---
|
||||
|
||||
// --- 2. SINCRONIZAR ÁMBITOS Y AGRUPACIONES ---
|
||||
// Cargamos los catálogos existentes en memoria UNA SOLA VEZ para eficiencia.
|
||||
// CORRECCIÓN: La siguiente línea faltaba por completo, causando el error CS0103.
|
||||
// Carga todos los ámbitos existentes en un diccionario para una verificación rápida.
|
||||
var ambitosEnDb = await dbContext.AmbitosGeograficos.ToDictionaryAsync(
|
||||
a => (a.DistritoId, a.SeccionId, a.MunicipioId, a.CircuitoId, a.EstablecimientoId),
|
||||
a => a, stoppingToken);
|
||||
a => a,
|
||||
stoppingToken
|
||||
);
|
||||
|
||||
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
|
||||
|
||||
@@ -206,13 +132,12 @@ public class Worker : BackgroundService
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
_logger.LogInformation("--- Sincronizando datos para la categoría: {Nombre} (ID: {Id}) ---", categoria.Nombre, categoria.CategoriaId);
|
||||
|
||||
var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(_authToken, categoria.CategoriaId);
|
||||
var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId);
|
||||
if (catalogoDto != null)
|
||||
{
|
||||
// SINCRONIZAR ÁMBITOS
|
||||
foreach (var ambitoDto in catalogoDto.Ambitos)
|
||||
{
|
||||
// CLAVE ÚNICA CORREGIDA Y MÁS COMPLETA
|
||||
var claveUnica = (
|
||||
ambitoDto.CodigoAmbitos.DistritoId,
|
||||
ambitoDto.CodigoAmbitos.SeccionId,
|
||||
@@ -221,6 +146,7 @@ public class Worker : BackgroundService
|
||||
ambitoDto.CodigoAmbitos.EstablecimientoId
|
||||
);
|
||||
|
||||
// Aquí se usaba 'ambitosEnDb' sin haberlo declarado. Ahora funciona.
|
||||
if (!ambitosEnDb.ContainsKey(claveUnica))
|
||||
{
|
||||
var nuevoAmbito = new AmbitoGeografico
|
||||
@@ -235,32 +161,40 @@ public class Worker : BackgroundService
|
||||
SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId
|
||||
};
|
||||
dbContext.AmbitosGeograficos.Add(nuevoAmbito);
|
||||
ambitosEnDb.Add(claveUnica, nuevoAmbito); // Añadimos al diccionario en memoria para evitar duplicados en el mismo ciclo
|
||||
// Se añade al diccionario en memoria para evitar duplicados en el mismo ciclo.
|
||||
ambitosEnDb.Add(claveUnica, nuevoAmbito);
|
||||
}
|
||||
}
|
||||
|
||||
// SINCRONIZAR AGRUPACIONES
|
||||
// Lógica para sincronizar AGRUPACIONES POLÍTICAS
|
||||
var provincia = catalogoDto.Ambitos.FirstOrDefault(a => a.NivelId == 10);
|
||||
if (provincia != null && !string.IsNullOrEmpty(provincia.CodigoAmbitos.DistritoId))
|
||||
{
|
||||
var agrupacionesApi = await _apiService.GetAgrupacionesAsync(_authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId);
|
||||
if (agrupacionesApi != null && agrupacionesApi.Any())
|
||||
try
|
||||
{
|
||||
foreach (var agrupacionDto in agrupacionesApi)
|
||||
var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId);
|
||||
if (agrupacionesApi != null && agrupacionesApi.Any())
|
||||
{
|
||||
if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
|
||||
foreach (var agrupacionDto in agrupacionesApi)
|
||||
{
|
||||
var nuevaAgrupacion = new AgrupacionPolitica
|
||||
if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
|
||||
{
|
||||
Id = agrupacionDto.IdAgrupacion,
|
||||
IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
|
||||
Nombre = agrupacionDto.NombreAgrupacion
|
||||
};
|
||||
dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion);
|
||||
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion);
|
||||
var nuevaAgrupacion = new AgrupacionPolitica
|
||||
{
|
||||
Id = agrupacionDto.IdAgrupacion,
|
||||
IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
|
||||
Nombre = agrupacionDto.NombreAgrupacion
|
||||
};
|
||||
dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion);
|
||||
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "No se pudieron obtener agrupaciones para la categoría '{catNombre}' ({catId}). Esto puede ser normal si la categoría no aplica a nivel provincial.", categoria.Nombre, categoria.CategoriaId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,20 +210,130 @@ public class Worker : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SondearBancasAsync(CancellationToken stoppingToken)
|
||||
// El resto de los métodos (SondearResultadosMunicipalesAsync, GuardarResultadosDeAmbitoAsync, etc.)
|
||||
// se mantienen como en la versión anterior que te proporcioné. Los incluyo aquí para
|
||||
// que tengas el archivo completo y sin errores.
|
||||
private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ObtenerTokenSiEsNecesario(stoppingToken);
|
||||
if (string.IsNullOrEmpty(_authToken) || stoppingToken.IsCancellationRequested) return;
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||
|
||||
// Obtenemos las secciones electorales (donde se reparten bancas de diputados)
|
||||
// Buscamos los ámbitos de nivel Municipio/Partido
|
||||
var municipiosASondear = await dbContext.AmbitosGeograficos
|
||||
.AsNoTracking()
|
||||
.Where(a => a.NivelId == 5 && a.MunicipioId != null && a.DistritoId != null && a.SeccionId != null)
|
||||
.Select(a => new { a.Id, a.MunicipioId, a.SeccionId, a.DistritoId })
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
if (!municipiosASondear.Any()) return;
|
||||
|
||||
_logger.LogInformation("Iniciando sondeo de resultados para {count} municipios (Partidos)...", municipiosASondear.Count);
|
||||
|
||||
foreach (var municipio in municipiosASondear)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
var categoriaConcejales = await dbContext.CategoriasElectorales
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Nombre.Contains("CONCEJALES"), stoppingToken);
|
||||
|
||||
if (categoriaConcejales != null)
|
||||
{
|
||||
// Para obtener resultados del PARTIDO completo, pasamos 'municipioId' como null.
|
||||
// Usamos el 'seccionId' del registro, que según la aclaración, es el ID del Partido.
|
||||
var resultados = await _apiService.GetResultadosAsync(
|
||||
authToken,
|
||||
municipio.DistritoId!,
|
||||
municipio.SeccionId!,
|
||||
null, // <- AHORA ES NULL
|
||||
categoriaConcejales.Id
|
||||
);
|
||||
|
||||
if (resultados != null)
|
||||
{
|
||||
await GuardarResultadosDeAmbitoAsync(dbContext, municipio.Id, resultados, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GuardarResultadosDeAmbitoAsync(EleccionesDbContext dbContext, int ambitoId, Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken)
|
||||
{
|
||||
var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId }, cancellationToken: stoppingToken);
|
||||
if (estadoRecuento == null)
|
||||
{
|
||||
estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId };
|
||||
dbContext.EstadosRecuentos.Add(estadoRecuento);
|
||||
}
|
||||
|
||||
estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime();
|
||||
estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas;
|
||||
estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas;
|
||||
estadoRecuento.MesasTotalizadasPorcentaje = resultadosDto.EstadoRecuento.MesasTotalizadasPorcentaje;
|
||||
estadoRecuento.CantidadElectores = resultadosDto.EstadoRecuento.CantidadElectores;
|
||||
estadoRecuento.CantidadVotantes = resultadosDto.EstadoRecuento.CantidadVotantes;
|
||||
estadoRecuento.ParticipacionPorcentaje = resultadosDto.EstadoRecuento.ParticipacionPorcentaje;
|
||||
|
||||
if (resultadosDto.ValoresTotalizadosOtros != null)
|
||||
{
|
||||
estadoRecuento.VotosEnBlanco = resultadosDto.ValoresTotalizadosOtros.VotosEnBlanco;
|
||||
estadoRecuento.VotosEnBlancoPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosEnBlancoPorcentaje;
|
||||
estadoRecuento.VotosNulos = resultadosDto.ValoresTotalizadosOtros.VotosNulos;
|
||||
estadoRecuento.VotosNulosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosNulosPorcentaje;
|
||||
estadoRecuento.VotosRecurridos = resultadosDto.ValoresTotalizadosOtros.VotosRecurridos;
|
||||
estadoRecuento.VotosRecurridosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosRecurridosPorcentaje;
|
||||
}
|
||||
|
||||
foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos)
|
||||
{
|
||||
var resultadoVoto = await dbContext.ResultadosVotos.FirstOrDefaultAsync(
|
||||
rv => rv.AmbitoGeograficoId == ambitoId && rv.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion,
|
||||
stoppingToken
|
||||
);
|
||||
|
||||
if (resultadoVoto == null)
|
||||
{
|
||||
resultadoVoto = new ResultadoVoto
|
||||
{
|
||||
AmbitoGeograficoId = ambitoId,
|
||||
AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion
|
||||
};
|
||||
dbContext.ResultadosVotos.Add(resultadoVoto);
|
||||
}
|
||||
resultadoVoto.CantidadVotos = votoPositivoDto.Votos;
|
||||
resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje;
|
||||
}
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
}
|
||||
|
||||
private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||
|
||||
var categoriasDeBancas = await dbContext.CategoriasElectorales
|
||||
.AsNoTracking()
|
||||
.Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS"))
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
if (!categoriasDeBancas.Any())
|
||||
{
|
||||
_logger.LogWarning("No se encontraron categorías para 'Senadores' o 'Diputados' en la BD. Omitiendo sondeo de bancas.");
|
||||
return;
|
||||
}
|
||||
|
||||
var secciones = await dbContext.AmbitosGeograficos
|
||||
.AsNoTracking()
|
||||
.Where(a => a.NivelId == 4 && a.DistritoId != null && a.SeccionId != null) // Nivel 4 = Sección Electoral
|
||||
.Where(a => a.NivelId == 4 && a.DistritoId != null && a.SeccionId != null)
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
if (!secciones.Any())
|
||||
@@ -298,53 +342,43 @@ public class Worker : BackgroundService
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Iniciando sondeo de Bancas para {count} secciones...", secciones.Count);
|
||||
|
||||
// Esta bandera nos asegura que solo borramos la tabla una vez y solo si hay datos nuevos.
|
||||
bool hasReceivedNewData = false;
|
||||
_logger.LogInformation("Iniciando sondeo de Bancas para {count} secciones y {catCount} categorías...", secciones.Count, categoriasDeBancas.Count);
|
||||
|
||||
bool hasReceivedAnyNewData = false;
|
||||
foreach (var seccion in secciones)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
var repartoBancas = await _apiService.GetBancasAsync(_authToken, seccion.DistritoId!, seccion.SeccionId!);
|
||||
|
||||
// Verificamos que la respuesta no sea nula y que la lista de bancas contenga al menos un elemento.
|
||||
if (repartoBancas?.RepartoBancas is { Count: > 0 })
|
||||
foreach (var categoria in categoriasDeBancas)
|
||||
{
|
||||
// Si esta es la primera vez en este ciclo de sondeo que recibimos datos válidos,
|
||||
// borramos todos los datos viejos de la tabla.
|
||||
if (!hasReceivedNewData)
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
var repartoBancas = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionId!, categoria.Id);
|
||||
if (repartoBancas?.RepartoBancas is { Count: > 0 })
|
||||
{
|
||||
_logger.LogInformation("Se recibieron nuevos datos de bancas. Limpiando tabla de proyecciones...");
|
||||
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
|
||||
hasReceivedNewData = true; // Marcamos que ya hemos limpiado la tabla.
|
||||
}
|
||||
|
||||
// Procedemos a añadir las nuevas proyecciones a la sesión de EF Core.
|
||||
foreach (var banca in repartoBancas.RepartoBancas)
|
||||
{
|
||||
var nuevaProyeccion = new ProyeccionBanca
|
||||
if (!hasReceivedAnyNewData)
|
||||
{
|
||||
AmbitoGeograficoId = seccion.Id,
|
||||
AgrupacionPoliticaId = banca.IdAgrupacion,
|
||||
NroBancas = banca.NroBancas
|
||||
};
|
||||
await dbContext.ProyeccionesBancas.AddAsync(nuevaProyeccion, stoppingToken);
|
||||
_logger.LogInformation("Se recibieron nuevos datos de bancas. Limpiando la tabla de proyecciones para evitar duplicados...");
|
||||
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
|
||||
hasReceivedAnyNewData = true;
|
||||
}
|
||||
|
||||
foreach (var banca in repartoBancas.RepartoBancas)
|
||||
{
|
||||
var nuevaProyeccion = new ProyeccionBanca
|
||||
{
|
||||
AmbitoGeograficoId = seccion.Id,
|
||||
AgrupacionPoliticaId = banca.IdAgrupacion,
|
||||
NroBancas = banca.NroBancas
|
||||
};
|
||||
await dbContext.ProyeccionesBancas.AddAsync(nuevaProyeccion, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No se recibieron datos de bancas para la sección {seccionId}.", seccion.SeccionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Si hemos añadido alguna entidad nueva (es decir, hasReceivedNewData es true),
|
||||
// guardamos todos los cambios en la base de datos.
|
||||
if (hasReceivedNewData)
|
||||
if (hasReceivedAnyNewData)
|
||||
{
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
_logger.LogInformation("Sondeo de Bancas completado. La tabla de proyecciones ha sido actualizada.");
|
||||
_logger.LogInformation("Sondeo de Bancas completado. La tabla de proyecciones ha sido actualizada con nuevos datos.");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -353,18 +387,14 @@ public class Worker : BackgroundService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ocurrió un error en el sondeo de Bancas.");
|
||||
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SondearTelegramasAsync(CancellationToken stoppingToken)
|
||||
private async Task SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ObtenerTokenSiEsNecesario(stoppingToken);
|
||||
if (string.IsNullOrEmpty(_authToken) || stoppingToken.IsCancellationRequested) return;
|
||||
|
||||
// --- CADA SONDEO USA SU PROPIO DBCONTEXT FRESCO ---
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||
|
||||
@@ -373,7 +403,11 @@ public class Worker : BackgroundService
|
||||
.Where(a => a.NivelId == 4 && a.DistritoId != null && a.SeccionId != null)
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
if (!secciones.Any()) return;
|
||||
if (!secciones.Any())
|
||||
{
|
||||
_logger.LogWarning("No hay Secciones Electorales en la BD para sondear telegramas.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Iniciando sondeo de Telegramas nuevos...");
|
||||
|
||||
@@ -381,78 +415,63 @@ public class Worker : BackgroundService
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(_authToken, seccion.DistritoId!, seccion.SeccionId!);
|
||||
var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, seccion.DistritoId!, seccion.SeccionId!);
|
||||
if (listaTelegramasApi is { Count: > 0 })
|
||||
{
|
||||
var idsDeApi = listaTelegramasApi.Select(t => t[0]).Distinct().ToList();
|
||||
|
||||
// --- LÓGICA DE DUPLICADOS ---
|
||||
// Consultamos a la base de datos por los IDs que la API nos acaba de dar
|
||||
var idsYaEnDb = await dbContext.Telegramas
|
||||
.Where(t => idsDeApi.Contains(t.Id))
|
||||
.Select(t => t.Id)
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
// Comparamos las dos listas para encontrar los que realmente son nuevos
|
||||
var nuevosTelegramasIds = idsDeApi.Except(idsYaEnDb).ToList();
|
||||
|
||||
if (!nuevosTelegramasIds.Any())
|
||||
{
|
||||
_logger.LogInformation("No hay telegramas nuevos para la sección {seccionId}.", seccion.SeccionId);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Se encontraron {count} telegramas nuevos en la sección {seccionId}. Descargando...", nuevosTelegramasIds.Count, seccion.SeccionId);
|
||||
|
||||
foreach (var mesaId in nuevosTelegramasIds)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
var telegramaFile = await _apiService.GetTelegramaFileAsync(_authToken, mesaId);
|
||||
var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId);
|
||||
if (telegramaFile != null)
|
||||
{
|
||||
var nuevoTelegrama = new Telegrama
|
||||
{
|
||||
Id = telegramaFile.NombreArchivo,
|
||||
AmbitoGeograficoId = seccion.Id, // Lo asociamos a la sección por simplicidad
|
||||
AmbitoGeograficoId = seccion.Id,
|
||||
ContenidoBase64 = telegramaFile.Imagen,
|
||||
FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(),
|
||||
FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime()
|
||||
};
|
||||
// Como estamos en un DbContext fresco, AddAsync no dará conflicto
|
||||
await dbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken);
|
||||
}
|
||||
}
|
||||
// Guardamos los cambios al final de cada sección procesada
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Sondeo de Telegramas completado.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ocurrió un error en el sondeo de Telegramas.");
|
||||
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Telegramas.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SondearResumenProvincialAsync(CancellationToken stoppingToken)
|
||||
private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ObtenerTokenSiEsNecesario(stoppingToken);
|
||||
if (string.IsNullOrEmpty(_authToken)) return;
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||
|
||||
var provincia = await dbContext.AmbitosGeograficos.AsNoTracking().FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);
|
||||
if (provincia == null) return;
|
||||
|
||||
var resumen = await _apiService.GetResumenAsync(_authToken, provincia.DistritoId!);
|
||||
var resumen = await _apiService.GetResumenAsync(authToken, provincia.DistritoId!);
|
||||
if (resumen?.ValoresTotalizadosPositivos is { Count: > 0 })
|
||||
{
|
||||
// Estrategia: Reemplazo completo
|
||||
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ResumenesVotos", stoppingToken);
|
||||
foreach (var voto in resumen.ValoresTotalizadosPositivos)
|
||||
{
|
||||
@@ -474,44 +493,68 @@ public class Worker : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SondearEstadoRecuentoGeneralAsync(CancellationToken stoppingToken)
|
||||
private async Task SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ObtenerTokenSiEsNecesario(stoppingToken);
|
||||
if (string.IsNullOrEmpty(_authToken)) return;
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||
|
||||
var provincia = await dbContext.AmbitosGeograficos.AsNoTracking().FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);
|
||||
if (provincia == null) return;
|
||||
|
||||
var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(_authToken, provincia.DistritoId!);
|
||||
if (estadoDto != null)
|
||||
var provincia = await dbContext.AmbitosGeograficos
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);
|
||||
if (provincia == null)
|
||||
{
|
||||
// Estrategia: Upsert
|
||||
var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(provincia.Id);
|
||||
if (registroDb == null)
|
||||
{
|
||||
registroDb = new EstadoRecuentoGeneral { AmbitoGeograficoId = provincia.Id };
|
||||
dbContext.EstadosRecuentosGenerales.Add(registroDb);
|
||||
}
|
||||
|
||||
registroDb.MesasEsperadas = estadoDto.MesasEsperadas;
|
||||
registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas;
|
||||
registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje;
|
||||
registroDb.CantidadElectores = estadoDto.CantidadElectores;
|
||||
registroDb.CantidadVotantes = estadoDto.CantidadVotantes;
|
||||
registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje;
|
||||
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
_logger.LogInformation("Sondeo de Estado Recuento General completado.");
|
||||
_logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) en la BD. Omitiendo sondeo de estado general.");
|
||||
return;
|
||||
}
|
||||
|
||||
var categoriasParaSondear = await dbContext.CategoriasElectorales
|
||||
.AsNoTracking()
|
||||
.ToListAsync(stoppingToken);
|
||||
if (!categoriasParaSondear.Any())
|
||||
{
|
||||
_logger.LogWarning("No hay categorías en la BD para sondear el estado general del recuento.");
|
||||
return;
|
||||
}
|
||||
_logger.LogInformation("Iniciando sondeo de Estado Recuento General para {count} categorías...", categoriasParaSondear.Count);
|
||||
|
||||
foreach (var categoria in categoriasParaSondear)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id);
|
||||
if (estadoDto != null)
|
||||
{
|
||||
var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(
|
||||
new object[] { provincia.Id, categoria.Id },
|
||||
cancellationToken: stoppingToken
|
||||
);
|
||||
|
||||
if (registroDb == null)
|
||||
{
|
||||
registroDb = new EstadoRecuentoGeneral
|
||||
{
|
||||
AmbitoGeograficoId = provincia.Id,
|
||||
CategoriaId = categoria.Id
|
||||
};
|
||||
dbContext.EstadosRecuentosGenerales.Add(registroDb);
|
||||
}
|
||||
|
||||
registroDb.MesasEsperadas = estadoDto.MesasEsperadas;
|
||||
registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas;
|
||||
registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje;
|
||||
registroDb.CantidadElectores = estadoDto.CantidadElectores;
|
||||
registroDb.CantidadVotantes = estadoDto.CantidadVotantes;
|
||||
registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje;
|
||||
}
|
||||
}
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
_logger.LogInformation("Sondeo de Estado Recuento General completado para todas las categorías.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ocurrió un error en el sondeo de Estado Recuento General.");
|
||||
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+69ddf2b2d24d4968c618c6fd9f38c1143625cdcd")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Worker")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Worker")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
Reference in New Issue
Block a user