Fix Worker

This commit is contained in:
2025-08-23 11:01:54 -03:00
parent 5de9d6729c
commit e5ecdc301e
17 changed files with 525 additions and 478 deletions

File diff suppressed because one or more lines are too long

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+18e6e8d3c0a378a172ad8e8afd31109673460717")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5de9d6729c40c2a0d191760aa879b57282ff37e1")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -173,7 +173,6 @@ E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Microsoft.
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Microsoft.Extensions.Diagnostics.Abstractions.dll
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Microsoft.Extensions.Http.dll
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Microsoft.Extensions.Options.ConfigurationExtensions.dll
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\buenos-aires-municipios.geojson
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Serilog.dll
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Serilog.AspNetCore.dll
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Serilog.Extensions.Hosting.dll

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Dji\u002Bta/0e7zUKw3oe\u002BriV3kbWxZ93FP2z2QIYsHXTl4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","42z\u002Bw0pajpMLFLNS29VoU/hUn9IzvZ/pVLNadS0rApY=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","ifGdmI/zx2hsc8PYkk8IWTP8aZ9RYmaQbfk383bAiYQ="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Dji\u002Bta/0e7zUKw3oe\u002BriV3kbWxZ93FP2z2QIYsHXTl4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","GjNU75/P064gZSGHRGFQCzuU1j91Y6ISVIG9G9dpcs0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","Oxph25v5ZpGJ3gq0\u002BdtskQ7kRY5YG8YA8z83gWvNxyw="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Dji\u002Bta/0e7zUKw3oe\u002BriV3kbWxZ93FP2z2QIYsHXTl4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","42z\u002Bw0pajpMLFLNS29VoU/hUn9IzvZ/pVLNadS0rApY=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","ifGdmI/zx2hsc8PYkk8IWTP8aZ9RYmaQbfk383bAiYQ="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Dji\u002Bta/0e7zUKw3oe\u002BriV3kbWxZ93FP2z2QIYsHXTl4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","GjNU75/P064gZSGHRGFQCzuU1j91Y6ISVIG9G9dpcs0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","Oxph25v5ZpGJ3gq0\u002BdtskQ7kRY5YG8YA8z83gWvNxyw="],"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+18e6e8d3c0a378a172ad8e8afd31109673460717")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5de9d6729c40c2a0d191760aa879b57282ff37e1")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -27,6 +27,7 @@ public class EleccionesDbContext(DbContextOptions<EleccionesDbContext> options)
// Precisión para los campos de porcentaje en EstadoRecuento
modelBuilder.Entity<EstadoRecuento>(entity =>
{
entity.HasKey(e => new { e.AmbitoGeograficoId, e.CategoriaId });
entity.Property(e => e.MesasTotalizadasPorcentaje).HasPrecision(5, 2);
entity.Property(e => e.ParticipacionPorcentaje).HasPrecision(5, 2);
entity.Property(e => e.VotosNulosPorcentaje).HasPrecision(18, 4);

View File

@@ -8,9 +8,9 @@ public class EstadoRecuento
{
[Key]
public int AmbitoGeograficoId { get; set; }
public int CategoriaId { get; set; }
[ForeignKey("AmbitoGeograficoId")]
public AmbitoGeografico AmbitoGeografico { get; set; } = null!;
public DateTime FechaTotalizacion { get; set; }
public int MesasEsperadas { get; set; }
public int MesasTotalizadas { get; set; }

View File

@@ -11,6 +11,8 @@ public class ResultadoVoto
public long Id { get; set; }
public int AmbitoGeograficoId { get; set; }
public int CategoriaId { get; set; }
[ForeignKey("AmbitoGeograficoId")]
public AmbitoGeografico AmbitoGeografico { get; set; } = null!;

View File

@@ -0,0 +1,370 @@
// <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("20250823135433_AddCategoriaIdToResultados")]
partial class AddCategoriaIdToResultados
{
/// <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>("CategoriaId")
.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", "CategoriaId");
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<int>("CategoriaId")
.HasColumnType("int");
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,58 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Elecciones.Database.Migrations
{
/// <inheritdoc />
public partial class AddCategoriaIdToResultados : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_EstadosRecuentos",
table: "EstadosRecuentos");
migrationBuilder.AddColumn<int>(
name: "CategoriaId",
table: "ResultadosVotos",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "CategoriaId",
table: "EstadosRecuentos",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddPrimaryKey(
name: "PK_EstadosRecuentos",
table: "EstadosRecuentos",
columns: new[] { "AmbitoGeograficoId", "CategoriaId" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_EstadosRecuentos",
table: "EstadosRecuentos");
migrationBuilder.DropColumn(
name: "CategoriaId",
table: "ResultadosVotos");
migrationBuilder.DropColumn(
name: "CategoriaId",
table: "EstadosRecuentos");
migrationBuilder.AddPrimaryKey(
name: "PK_EstadosRecuentos",
table: "EstadosRecuentos",
column: "AmbitoGeograficoId");
}
}
}

View File

@@ -103,6 +103,9 @@ namespace Elecciones.Database.Migrations
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<int>("CantidadElectores")
.HasColumnType("int");
@@ -147,7 +150,7 @@ namespace Elecciones.Database.Migrations
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.HasKey("AmbitoGeograficoId");
b.HasKey("AmbitoGeograficoId", "CategoriaId");
b.ToTable("EstadosRecuentos");
});
@@ -232,6 +235,9 @@ namespace Elecciones.Database.Migrations
b.Property<long>("CantidadVotos")
.HasColumnType("bigint");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<decimal>("PorcentajeVotos")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");

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+18e6e8d3c0a378a172ad8e8afd31109673460717")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5de9d6729c40c2a0d191760aa879b57282ff37e1")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

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+18e6e8d3c0a378a172ad8e8afd31109673460717")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5de9d6729c40c2a0d191760aa879b57282ff37e1")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -13,4 +13,3 @@ E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\obj\Debug\net9.0
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\obj\Debug\net9.0\refint\Elecciones.Infrastructure.dll
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\obj\Debug\net9.0\Elecciones.Infrastructure.pdb
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\obj\Debug\net9.0\ref\Elecciones.Infrastructure.dll
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\bin\Debug\net9.0\buenos-aires-municipios.geojson

View File

@@ -11,38 +11,35 @@ public class CriticalDataWorker : BackgroundService
private readonly ILogger<CriticalDataWorker> _logger;
private readonly SharedTokenService _tokenService;
private readonly IServiceProvider _serviceProvider;
private readonly IElectoralApiService _apiService; // <-- DEPENDENCIA AÑADIDA
private readonly IElectoralApiService _apiService;
// Inyectamos IElectoralApiService en el constructor
public CriticalDataWorker(
ILogger<CriticalDataWorker> logger,
SharedTokenService tokenService,
IServiceProvider serviceProvider,
IElectoralApiService apiService) // <-- PARÁMETRO AÑADIDO
IElectoralApiService apiService)
{
_logger = logger;
_tokenService = tokenService;
_serviceProvider = serviceProvider;
_apiService = apiService; // <-- ASIGNACIÓN AÑADIDA
_apiService = apiService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker de Datos Críticos iniciado.");
// Damos tiempo a la sincronización inicial del otro worker para que se complete.
try
{
await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken);
}
catch (TaskCanceledException) { return; } // Salir si la app se apaga durante la espera inicial
catch (TaskCanceledException) { return; }
int cicloContador = 0;
while (!stoppingToken.IsCancellationRequested)
{
var cicloInicio = DateTime.UtcNow;
cicloContador++;
_logger.LogInformation("--- Iniciando Ciclo de Datos Críticos #{ciclo} ---", cicloContador);
var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken);
@@ -68,200 +65,122 @@ public class CriticalDataWorker : BackgroundService
{
await Task.Delay(tiempoDeEspera, stoppingToken);
}
catch (TaskCanceledException)
{
break;
}
catch (TaskCanceledException) { break; }
}
}
/// <summary>
/// Sondea los resultados electorales para todos los municipios/partidos de forma optimizada.
/// Utiliza paralelismo controlado para ejecutar múltiples peticiones a la API simultáneamente
/// sin sobrecargar la red, y luego guarda todos los resultados en la base de datos de forma masiva.
/// </summary>
/// <param name="authToken">El token de autenticación válido para la sesión.</param>
/// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken)
{
try
{
// PASO 1: Preparar el DbContext y los datos necesarios.
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
// Obtenemos de nuestra BD local la lista de todos los partidos (NivelId=30) que necesitamos consultar.
var municipiosASondear = await dbContext.AmbitosGeograficos
.AsNoTracking()
.Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null)
.Select(a => new { a.Id, a.Nombre, a.MunicipioId, a.SeccionId, a.DistritoId })
.ToListAsync(stoppingToken);
if (!municipiosASondear.Any())
{
_logger.LogWarning("No se encontraron Partidos (NivelId 30) en la BD para sondear resultados.");
return;
}
// Obtenemos la categoría "CONCEJALES", ya que los resultados municipales aplican a esta.
var categoriaConcejales = await dbContext.CategoriasElectorales
var todasLasCategorias = await dbContext.CategoriasElectorales
.AsNoTracking()
.FirstOrDefaultAsync(c => c.Nombre.Contains("CONCEJALES"), stoppingToken);
.ToListAsync(stoppingToken);
if (categoriaConcejales == null)
if (!municipiosASondear.Any() || !todasLasCategorias.Any())
{
_logger.LogWarning("No se encontró la categoría 'CONCEJALES'. Omitiendo sondeo de resultados municipales.");
_logger.LogWarning("No se encontraron Partidos (NivelId 30) o Categorías para sondear resultados.");
return;
}
// PASO 2: Ejecutar las consultas a la API con paralelismo controlado.
_logger.LogInformation("Iniciando sondeo de resultados para {m} municipios y {c} categorías...", municipiosASondear.Count, todasLasCategorias.Count);
// Definimos cuántas peticiones queremos que se ejecuten simultáneamente.
// Un valor entre 8 y 16 es generalmente seguro y ofrece una gran mejora de velocidad.
const int GRADO_DE_PARALELISMO = 3;
// Creamos un semáforo que actuará como un "control de acceso" con 10 pases libres.
var semaforo = new SemaphoreSlim(GRADO_DE_PARALELISMO);
// Usamos un ConcurrentDictionary para almacenar los resultados. A diferencia de un Dictionary normal,
// este permite que múltiples tareas escriban en él al mismo tiempo sin conflictos.
var resultadosPorId = new ConcurrentDictionary<int, Elecciones.Core.DTOs.ResultadosDto>();
_logger.LogInformation("Iniciando sondeo de resultados para {count} municipios con un paralelismo de {degree}...", municipiosASondear.Count, GRADO_DE_PARALELISMO);
// Creamos una lista de tareas (Tasks), una por cada municipio a consultar.
// El método .Select() no ejecuta las tareas todavía, solo las prepara.
var tareas = municipiosASondear.Select(async municipio =>
foreach (var municipio in municipiosASondear)
{
// Cada tarea debe "pedir permiso" al semáforo antes de ejecutarse.
// Si ya hay 10 tareas en ejecución, esta línea esperará hasta que una termine.
await semaforo.WaitAsync(stoppingToken);
try
{
// Una vez que obtiene el permiso, ejecuta la petición a la API.
var resultados = await _apiService.GetResultadosAsync(
authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoriaConcejales.Id
);
if (stoppingToken.IsCancellationRequested) break;
var tareasCategoria = todasLasCategorias.Select(async categoria =>
{
var resultados = await _apiService.GetResultadosAsync(authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoria.Id);
// Si la API devuelve datos válidos...
if (resultados != null)
{
// ...los guardamos en el diccionario concurrente.
resultadosPorId[municipio.Id] = resultados;
using var innerScope = _serviceProvider.CreateScope();
var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
// --- LLAMADA CORRECTA ---
await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken);
}
}
finally
{
// ¡CRUCIAL! Liberamos el pase del semáforo, permitiendo que la siguiente
// tarea en espera pueda comenzar su ejecución.
semaforo.Release();
// Añadir un pequeño retraso aleatorio para no parecer un robot
await Task.Delay(TimeSpan.FromMilliseconds(new Random().Next(50, 251)), stoppingToken);
}
});
});
// Ahora sí, ejecutamos todas las tareas preparadas en paralelo y esperamos a que todas terminen.
await Task.WhenAll(tareas);
// PASO 3: Guardar los resultados en la base de datos.
// Solo procedemos si recolectamos al menos un resultado válido.
if (resultadosPorId.Any())
{
// Llamamos a nuestro método de guardado masivo y optimizado, pasándole todos los resultados
// recolectados para que los inserte en una única y eficiente transacción.
await GuardarResultadosDeMunicipiosAsync(dbContext, resultadosPorId.ToDictionary(kv => kv.Key, kv => kv.Value), stoppingToken);
await Task.WhenAll(tareasCategoria);
}
}
catch (Exception ex)
{
// Capturamos cualquier error inesperado en el proceso para que el worker no se detenga.
_logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales.");
}
}
/// Realiza una operación "Upsert" (Update o Insert) de forma masiva y optimizada.
/// Este método es llamado por SondearResultadosMunicipalesAsync.
/// </summary>
private async Task GuardarResultadosDeMunicipiosAsync(
EleccionesDbContext dbContext,
Dictionary<int, Elecciones.Core.DTOs.ResultadosDto> todosLosResultados,
CancellationToken stoppingToken) // <-- PARÁMETRO AÑADIDO
private async Task GuardarResultadosDeAmbitoAsync(
EleccionesDbContext dbContext, int ambitoId, int categoriaId,
Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken)
{
// Obtenemos los IDs de todos los ámbitos que vamos a actualizar.
var ambitoIds = todosLosResultados.Keys;
var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken);
// --- OPTIMIZACIÓN 1: Cargar todos los datos existentes en memoria UNA SOLA VEZ ---
var estadosRecuentoExistentes = await dbContext.EstadosRecuentos
.Where(e => ambitoIds.Contains(e.AmbitoGeograficoId))
.ToDictionaryAsync(e => e.AmbitoGeograficoId, stoppingToken);
var resultadosVotosExistentes = await dbContext.ResultadosVotos
.Where(rv => ambitoIds.Contains(rv.AmbitoGeograficoId))
.GroupBy(rv => rv.AmbitoGeograficoId)
.ToDictionaryAsync(g => g.Key, g => g.ToDictionary(item => item.AgrupacionPoliticaId), stoppingToken);
_logger.LogInformation("Procesando en memoria los resultados de {count} municipios.", todosLosResultados.Count);
// --- OPTIMIZACIÓN 2: Procesar todo en memoria ---
foreach (var kvp in todosLosResultados)
if (estadoRecuento == null)
{
var ambitoId = kvp.Key;
var resultadosDto = kvp.Value;
// Lógica Upsert para EstadoRecuento
if (!estadosRecuentoExistentes.TryGetValue(ambitoId, out var estadoRecuento))
{
estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId };
dbContext.EstadosRecuentos.Add(estadoRecuento);
}
// Mapeo completo de propiedades para 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;
}
// Lógica Upsert para ResultadosVotos
var votosDeAmbitoExistentes = resultadosVotosExistentes.GetValueOrDefault(ambitoId);
foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos)
{
ResultadoVoto? resultadoVoto = null;
if (votosDeAmbitoExistentes != null)
{
votosDeAmbitoExistentes.TryGetValue(votoPositivoDto.IdAgrupacion, out resultadoVoto);
}
if (resultadoVoto == null)
{
resultadoVoto = new ResultadoVoto
{
AmbitoGeograficoId = ambitoId,
AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion
};
dbContext.ResultadosVotos.Add(resultadoVoto);
}
resultadoVoto.CantidadVotos = votoPositivoDto.Votos;
resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje;
}
estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId, CategoriaId = categoriaId };
dbContext.EstadosRecuentos.Add(estadoRecuento);
}
// --- OPTIMIZACIÓN 3: Guardar todos los cambios en UNA SOLA TRANSACCIÓN ---
_logger.LogInformation("Guardando todos los cambios de resultados municipales en la base de datos...");
// Ahora 'stoppingToken' es reconocido aquí
await dbContext.SaveChangesAsync(stoppingToken);
_logger.LogInformation("Guardado completado.");
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.CategoriaId == categoriaId &&
rv.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion,
stoppingToken
);
if (resultadoVoto == null)
{
resultadoVoto = new ResultadoVoto
{
AmbitoGeograficoId = ambitoId,
CategoriaId = categoriaId,
AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion
};
dbContext.ResultadosVotos.Add(resultadoVoto);
}
resultadoVoto.CantidadVotos = votoPositivoDto.Votos;
resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje;
}
try
{
await dbContext.SaveChangesAsync(stoppingToken);
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "DbUpdateException al guardar resultados para AmbitoId {ambitoId} y CategoriaId {categoriaId}", ambitoId, categoriaId);
}
}
/// <summary>
@@ -420,11 +339,4 @@ public class CriticalDataWorker : BackgroundService
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General.");
}
}
// Pega aquí los métodos:
// - SondearResultadosMunicipalesAsync
// - GuardarResultadosDeMunicipiosAsync
// - SondearResumenProvincialAsync
// - SondearEstadoRecuentoGeneralAsync
// (Estos métodos necesitan IServiceProvider y SharedTokenService, que ya están inyectados)
}