Retry Con Cambios Importantes.

This commit is contained in:
2025-08-17 20:08:38 -03:00
parent 30f1e751b7
commit 258add9305
15 changed files with 864 additions and 264 deletions

View File

@@ -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")]

View File

@@ -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":{}}

View File

@@ -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":{}}

View File

@@ -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")]

View File

@@ -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);
});
}
}

View File

@@ -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!;
}

View File

@@ -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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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")

View File

@@ -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")]

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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")]

View File

@@ -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.");
}
}
}

View File

@@ -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")]