From 069446b90326f5acee630bf3b88238e4e054764f Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Oct 2025 21:27:30 -0300 Subject: [PATCH] =?UTF-8?q?Fix=20Mapa=20Municipios=20-=20Limpieza=20y=20Op?= =?UTF-8?q?timizaci=C3=B3n=20de=20Workers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Elecciones-Web/frontend/src/apiService.ts | 1 - .../Controllers/ResultadosController.cs | 32 +- .../net9.0/Elecciones.Api.AssemblyInfo.cs | 2 +- .../Debug/net9.0/rjsmcshtml.dswa.cache.json | 2 +- .../Debug/net9.0/rjsmrazor.dswa.cache.json | 2 +- .../net9.0/Elecciones.Core.AssemblyInfo.cs | 2 +- .../Elecciones.Database.AssemblyInfo.cs | 2 +- .../Elecciones.Infrastructure.AssemblyInfo.cs | 2 +- .../Elecciones.Worker/CriticalDataWorker.cs | 597 +++------------- .../LowPriorityDataWorker.cs | 661 +++--------------- 10 files changed, 224 insertions(+), 1079 deletions(-) diff --git a/Elecciones-Web/frontend/src/apiService.ts b/Elecciones-Web/frontend/src/apiService.ts index a7dc479..be2a75a 100644 --- a/Elecciones-Web/frontend/src/apiService.ts +++ b/Elecciones-Web/frontend/src/apiService.ts @@ -324,7 +324,6 @@ export const getHomeResumenNacional = async (eleccionId: number, categoriaId: nu eleccionId: eleccionId.toString(), categoriaId: categoriaId.toString(), }); - // Apunta al nuevo endpoint que creamos const url = `/elecciones/home-resumen-nacional?${queryParams.toString()}`; const { data } = await apiClient.get(url); return data; diff --git a/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs b/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs index 3a72548..b027044 100644 --- a/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs +++ b/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs @@ -1011,24 +1011,29 @@ public class ResultadosController : ControllerBase [HttpGet("panel/{ambitoId?}")] public async Task GetPanelElectoral(int eleccionId, string? ambitoId, [FromQuery] int categoriaId) { + // Caso 1: Sin ID -> Vista Nacional if (string.IsNullOrEmpty(ambitoId)) { - // CASO 1: No hay ID -> Vista Nacional return await GetPanelNacional(eleccionId, categoriaId); } - // CASO 2: El ID es un número (y no un string corto como "02") -> Vista Municipal - // La condición clave es que los IDs de distrito son cortos. Los IDs de BD son más largos. - // O simplemente, un ID de distrito nunca será un ID de municipio. - if (int.TryParse(ambitoId, out int idNumerico) && ambitoId.Length > 2) + // Intenta interpretar el ambitoId como un ID numérico de municipio. + if (int.TryParse(ambitoId, out int idNumerico)) { - return await GetPanelMunicipal(eleccionId, idNumerico, categoriaId); - } - else - { - // CASO 3: El ID es un string corto como "02" o "06" -> Vista Provincial - return await GetPanelProvincial(eleccionId, ambitoId, categoriaId); + // Consulta a la BD para verificar si existe un municipio (NivelId=30) con este ID. + // Esta consulta es muy rápida y resuelve la ambigüedad. + bool esMunicipio = await _dbContext.AmbitosGeograficos + .AnyAsync(a => a.Id == idNumerico && a.NivelId == 30); + + if (esMunicipio) + { + // Si es un municipio válido, llama al método municipal. + return await GetPanelMunicipal(eleccionId, idNumerico, categoriaId); + } } + // Si no es un ID de municipio válido (o no es un número), + // se asume que es un distritoId provincial y se llama al método provincial. + return await GetPanelProvincial(eleccionId, ambitoId, categoriaId); } private async Task GetPanelMunicipal(int eleccionId, int ambitoId, int categoriaId) @@ -1179,7 +1184,6 @@ public class ResultadosController : ControllerBase var todosLosOverrides = await _dbContext.CandidatosOverrides.AsNoTracking().Where(c => c.EleccionId == eleccionId || c.EleccionId == 0).ToListAsync(); var todosLosLogos = await _dbContext.LogosAgrupacionesCategorias.AsNoTracking().Where(l => l.EleccionId == eleccionId || l.EleccionId == 0).ToListAsync(); - var resultadosAgregados = await _dbContext.ResultadosVotos.AsNoTracking() .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId) .GroupBy(r => r.AgrupacionPolitica) @@ -1202,7 +1206,7 @@ public class ResultadosController : ControllerBase Color = g.Agrupacion.Color, Votos = g.TotalVotos, Porcentaje = totalVotosNacional > 0 ? (g.TotalVotos / totalVotosNacional) * 100 : 0, - NombreCandidato = null, + NombreCandidato = candidatoMatch?.NombreCandidato, // Mantenemos la lógica de candidato por si se usa a futuro LogoUrl = logoMatch?.LogoUrl }; }) @@ -1210,7 +1214,7 @@ public class ResultadosController : ControllerBase .ToList(); var estadoRecuento = await _dbContext.EstadosRecuentosGenerales.AsNoTracking() - .FirstOrDefaultAsync(e => e.EleccionId == eleccionId && e.CategoriaId == categoriaId && e.AmbitoGeografico.NivelId == 0); + .FirstOrDefaultAsync(e => e.EleccionId == eleccionId && e.CategoriaId == categoriaId && (e.AmbitoGeografico.NivelId == 1 || e.AmbitoGeografico.NivelId == 0)); var respuesta = new PanelElectoralDto { diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs index 4e62aac..920b534 100644 --- a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs @@ -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+ae846f2d4834f3cd03079e91a8225e9f74cd073b")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2b7fb927e2f0d9ff06dffa820bc9809d6e138b01")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json index 1af09cc..8a3bd48 100644 --- a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json +++ b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","/UJFXzVO5Y6TKX\u002BD5m/A1RI/tbK98BAoQFTS7wCUAJI=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","YO7Xv4ZJWkAJrh9hWzESGMBLWiXQVbzGiJis/zKzy9k="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","8QWUReqP8upfOnmA5lMNgBxAfYJ1z3zv/WYBUXBEiog=","TKLv8wZzSutso/ZGIrOh0nruXDmGN\u002BWPGW7DXc2UOM8=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","9jkfcKbUkkVKrX75MjakO0Et3SVlP1YOXw\u002Bfn\u002B5Hu3g="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json index dddb5aa..017ba67 100644 --- a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json +++ b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","/UJFXzVO5Y6TKX\u002BD5m/A1RI/tbK98BAoQFTS7wCUAJI=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","YO7Xv4ZJWkAJrh9hWzESGMBLWiXQVbzGiJis/zKzy9k="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","8QWUReqP8upfOnmA5lMNgBxAfYJ1z3zv/WYBUXBEiog=","TKLv8wZzSutso/ZGIrOh0nruXDmGN\u002BWPGW7DXc2UOM8=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","9jkfcKbUkkVKrX75MjakO0Et3SVlP1YOXw\u002Bfn\u002B5Hu3g="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs index b24d21a..51fd506 100644 --- a/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs @@ -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+4bc257df43f5813ec432b89b47fa078c1cfa1fc8")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2b7fb927e2f0d9ff06dffa820bc9809d6e138b01")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs index 69346f6..805dede 100644 --- a/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs @@ -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+4bc257df43f5813ec432b89b47fa078c1cfa1fc8")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2b7fb927e2f0d9ff06dffa820bc9809d6e138b01")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs index 7895b9d..d0baabf 100644 --- a/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs @@ -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+4bc257df43f5813ec432b89b47fa078c1cfa1fc8")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2b7fb927e2f0d9ff06dffa820bc9809d6e138b01")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs b/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs index fc3e66b..cd2cf21 100644 --- a/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs +++ b/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs @@ -1,4 +1,5 @@ -//Elecciones.Worker/CriticalDataWorker.cs +// src/Elecciones.Worker/CriticalDataWorker.cs + using Elecciones.Database; using Elecciones.Database.Entities; using Elecciones.Infrastructure.Services; @@ -15,6 +16,7 @@ public class CriticalDataWorker : BackgroundService private readonly IServiceProvider _serviceProvider; private readonly IElectoralApiService _apiService; private readonly WorkerConfigService _configService; + private static readonly ConcurrentDictionary _lastUpdateTracker = new(); public CriticalDataWorker( ILogger logger, @@ -32,20 +34,18 @@ public class CriticalDataWorker : BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation("Worker de Datos Críticos iniciado."); + _logger.LogInformation("Worker de Datos Críticos (Sondeo Inteligente) iniciado."); + await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken); - try - { - await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken); - } - 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 settings = await _configService.GetSettingsAsync(); + if (!settings.ResultadosActivado) + { + _logger.LogInformation("El sondeo de resultados está desactivado. Esperando 1 minuto."); + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + continue; + } var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken); if (string.IsNullOrEmpty(authToken)) @@ -55,336 +55,160 @@ public class CriticalDataWorker : BackgroundService continue; } - var settings = await _configService.GetSettingsAsync(); - - if (settings.Prioridad == "Resultados" && settings.ResultadosActivado) - { - _logger.LogInformation("Ejecutando tareas de Resultados en alta prioridad."); - await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); - await SondearResultadosMunicipalesAsync(authToken, stoppingToken); - await SondearResumenProvincialAsync(authToken, stoppingToken); - } - else if (settings.Prioridad == "Telegramas" && settings.BajasActivado) - { - _logger.LogInformation("Ejecutando tareas de Baja Prioridad en alta prioridad."); - await SondearProyeccionBancasAsync(authToken, stoppingToken); - //await SondearNuevosTelegramasAsync(authToken, stoppingToken); - } - else - { - _logger.LogInformation("Worker de alta prioridad inactivo según la configuración."); - } - - var cicloFin = DateTime.UtcNow; - var duracionCiclo = cicloFin - cicloInicio; - _logger.LogInformation("--- Ciclo de Datos Críticos #{ciclo} completado en {duration:N2} segundos. ---", cicloContador, duracionCiclo.TotalSeconds); - - var tiempoDeEspera = TimeSpan.FromSeconds(30) - duracionCiclo; - if (tiempoDeEspera < TimeSpan.Zero) tiempoDeEspera = TimeSpan.Zero; + _logger.LogInformation("--- Iniciando Ciclo de Sondeo Inteligente ---"); + await SondearEstadosYDispararActualizaciones(authToken, stoppingToken); + _logger.LogInformation("--- Ciclo de Sondeo Inteligente completado. ---"); try { - await Task.Delay(tiempoDeEspera, stoppingToken); + await Task.Delay(TimeSpan.FromSeconds(45), stoppingToken); } catch (TaskCanceledException) { break; } } } - /// - /// Sondea la proyección de bancas a nivel Provincial y por Sección Electoral. - /// Esta versión es completamente robusta: maneja respuestas de API vacías o con fechas mal formadas, - /// guarda la CategoriaId y usa una transacción atómica para la escritura en base de datos. - /// - /// El token de autenticación válido para la sesión. - /// El token de cancelación para detener la operación. - private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) + private async Task SondearEstadosYDispararActualizaciones(string authToken, CancellationToken stoppingToken) { try { using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var categoriasDeBancas = await dbContext.CategoriasElectorales - .AsNoTracking() - .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) - .ToListAsync(stoppingToken); + var ambitosProvinciales = await dbContext.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 10 && a.DistritoId != null).ToListAsync(stoppingToken); + var ambitoNacional = await dbContext.AmbitosGeograficos.AsNoTracking().FirstOrDefaultAsync(a => a.NivelId == 1 || a.NivelId == 0, stoppingToken); + var categorias = await dbContext.CategoriasElectorales.AsNoTracking().ToListAsync(stoppingToken); - var ambitosASondear = await dbContext.AmbitosGeograficos - .AsNoTracking() - .Where(a => a.NivelId == 10 && a.DistritoId != null) - .ToListAsync(stoppingToken); + if (!ambitosProvinciales.Any() || !categorias.Any()) return; - if (!categoriasDeBancas.Any() || !ambitosASondear.Any()) - { - _logger.LogWarning("No se encontraron categorías de bancas o ámbitos provinciales en la BD. Omitiendo sondeo de bancas."); - return; - } - - _logger.LogInformation("Iniciando sondeo de Bancas a nivel Provincial para {count} provincias...", ambitosASondear.Count); - - var todasLasProyecciones = new List(); - bool hasReceivedAnyNewData = false; - var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); - - foreach (var ambito in ambitosASondear) + // --- PASO 1: SONDEO PROVINCIAL --- + foreach (var provincia in ambitosProvinciales) { if (stoppingToken.IsCancellationRequested) break; - foreach (var categoria in categoriasDeBancas) + foreach (var categoria in categorias) { if (stoppingToken.IsCancellationRequested) break; - var repartoBancasDto = await _apiService.GetBancasAsync(authToken, ambito.DistritoId!, ambito.SeccionProvincialId, categoria.Id); + var resultadosDto = await _apiService.GetResultadosAsync(authToken, provincia.DistritoId!, null, null, categoria.Id); + if (resultadosDto == null) continue; - if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) + // --- Guardar los datos agregados inmediatamente --- + await GuardarDatosAgregadosAsync(dbContext, provincia.Id, categoria.Id, resultadosDto, stoppingToken); + + if (DateTime.TryParse(resultadosDto.FechaTotalizacion, out var fechaApi)) { - hasReceivedAnyNewData = true; - DateTime fechaTotalizacion; - if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) - { - _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); - fechaTotalizacion = DateTime.UtcNow; - } - else - { - fechaTotalizacion = parsedDate.ToUniversalTime(); - } + var trackerKey = $"{provincia.Id}-{categoria.Id}"; + _lastUpdateTracker.TryGetValue(trackerKey, out var ultimaFechaConocida); - foreach (var banca in bancas) + if (fechaApi > ultimaFechaConocida) { - if (!agrupacionesEnDb.ContainsKey(banca.IdAgrupacion)) - { - _logger.LogWarning("Agrupación con ID {AgrupacionId} ('{Nombre}') no encontrada. Creándola desde los datos de bancas.", banca.IdAgrupacion, banca.NombreAgrupacion); - var nuevaAgrupacion = new AgrupacionPolitica - { - Id = banca.IdAgrupacion, - Nombre = banca.NombreAgrupacion, - IdTelegrama = banca.IdAgrupacionTelegrama ?? string.Empty - }; - await dbContext.AgrupacionesPoliticas.AddAsync(nuevaAgrupacion, stoppingToken); - agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); - } - - todasLasProyecciones.Add(new ProyeccionBanca - { - EleccionId = EleccionId, - AmbitoGeograficoId = ambito.Id, - AgrupacionPoliticaId = banca.IdAgrupacion, - NroBancas = banca.NroBancas, - CategoriaId = categoria.Id, - FechaTotalizacion = fechaTotalizacion - }); + _logger.LogInformation("¡Cambio detectado para {Provincia} - {Categoria}! Actualizando detalles en segundo plano...", provincia.Nombre, categoria.Nombre); + _lastUpdateTracker[trackerKey] = fechaApi; + _ = Task.Run(() => ActualizarResultadosDeProvincia(authToken, provincia.DistritoId!, categoria.Id), stoppingToken); } } } } - if (hasReceivedAnyNewData) - { - _logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos..."); - await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); - await dbContext.SaveChangesAsync(stoppingToken); // Guardar nuevas agrupaciones si las hay - await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas WHERE EleccionId = {0}", EleccionId, stoppingToken); // CORRECCIÓN: Borrar solo para la elección actual - await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken); - await dbContext.SaveChangesAsync(stoppingToken); - await transaction.CommitAsync(stoppingToken); - _logger.LogInformation("La tabla de proyecciones ha sido actualizada con {count} registros.", todasLasProyecciones.Count); - } - else - { - _logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos, la tabla no fue modificada."); - } - } - catch (OperationCanceledException) - { - _logger.LogInformation("Sondeo de bancas cancelado."); - } - catch (Exception ex) - { - _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas."); - } - } - - /// - /// Busca y descarga nuevos telegramas de forma masiva y concurrente. - /// Este método crea una lista de todas las combinaciones de Partido/Categoría, - /// las consulta a la API con un grado de paralelismo controlado, y cada tarea concurrente - /// maneja su propia lógica de descarga y guardado en la base de datos. - /// - /// El token de autenticación válido para la sesión. - /// El token de cancelación para detener la operación. - private async Task SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken) - { - try - { - _logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas (modo de bajo perfil) ---"); - - using var scope = _serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - // La obtención de partidos y categorías no cambia - var partidos = await dbContext.AmbitosGeograficos.AsNoTracking() - .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) - .ToListAsync(stoppingToken); - var categorias = await dbContext.CategoriasElectorales.AsNoTracking().ToListAsync(stoppingToken); - - if (!partidos.Any() || !categorias.Any()) return; - - foreach (var partido in partidos) + // --- PASO 2: SONDEO NACIONAL --- + if (ambitoNacional != null && !stoppingToken.IsCancellationRequested) { + _logger.LogInformation("Sondeando totales a nivel Nacional..."); foreach (var categoria in categorias) { - if (stoppingToken.IsCancellationRequested) return; - - var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id); - - if (listaTelegramasApi is { Count: > 0 }) + if (stoppingToken.IsCancellationRequested) break; + var resultadosNacionalDto = await _apiService.GetResultadosAsync(authToken, "", null, null, categoria.Id); + if (resultadosNacionalDto != null) { - // Creamos el DbContext para la operación de guardado - using var innerScope = _serviceProvider.CreateScope(); - var innerDbContext = innerScope.ServiceProvider.GetRequiredService(); - - var idsYaEnDb = await innerDbContext.Telegramas - .Where(t => listaTelegramasApi.Contains(t.Id)) - .Select(t => t.Id).ToListAsync(stoppingToken); - - var nuevosTelegramasIds = listaTelegramasApi.Except(idsYaEnDb).ToList(); - - if (nuevosTelegramasIds.Any()) - { - _logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando...", nuevosTelegramasIds.Count, partido.Nombre, categoria.Nombre); - - var originalTimeout = innerDbContext.Database.GetCommandTimeout(); - try - { - innerDbContext.Database.SetCommandTimeout(180); - _logger.LogDebug("Timeout de BD aumentado a 180s para descarga de telegramas."); - - int contadorLote = 0; - const int tamanoLote = 100; - - foreach (var mesaId in nuevosTelegramasIds) - { - if (stoppingToken.IsCancellationRequested) return; - - var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); - if (telegramaFile != null) - { - var ambitoMesa = await innerDbContext.AmbitosGeograficos.AsNoTracking() - .FirstOrDefaultAsync(a => a.MesaId == mesaId, stoppingToken); - - if (ambitoMesa != null) - { - var nuevoTelegrama = new Telegrama - { - EleccionId = EleccionId, - Id = telegramaFile.NombreArchivo, - AmbitoGeograficoId = ambitoMesa.Id, - ContenidoBase64 = telegramaFile.Imagen, - FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), - FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() - }; - await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken); - contadorLote++; - } - else - { - _logger.LogWarning("No se encontró un ámbito geográfico para la mesa con MesaId {MesaId}. El telegrama no será guardado.", mesaId); - } - } - await Task.Delay(250, stoppingToken); - - if (contadorLote >= tamanoLote) - { - await innerDbContext.SaveChangesAsync(stoppingToken); - _logger.LogInformation("Guardado un lote de {count} telegramas.", contadorLote); - contadorLote = 0; - } - } - - if (contadorLote > 0) - { - await innerDbContext.SaveChangesAsync(stoppingToken); - _logger.LogInformation("Guardado el último lote de {count} telegramas.", contadorLote); - } - } - finally - { - innerDbContext.Database.SetCommandTimeout(originalTimeout); - _logger.LogDebug("Timeout de BD restaurado a su valor original ({timeout}s).", originalTimeout); - } - } // Fin del if (nuevosTelegramasIds.Any()) - - // Movemos el delay aquí para que solo se ejecute si hubo telegramas en la respuesta de la API - await Task.Delay(100, stoppingToken); - } // Fin del if (listaTelegramasApi is not null) + await GuardarDatosAgregadosAsync(dbContext, ambitoNacional.Id, categoria.Id, resultadosNacionalDto, stoppingToken); + } } } - _logger.LogInformation("Sondeo de Telegramas completado."); - } - catch (OperationCanceledException) - { - _logger.LogInformation("Sondeo de telegramas cancelado."); } catch (Exception ex) { - _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Telegramas."); + _logger.LogError(ex, "Error en el ciclo de sondeo de estados."); } } - private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken) + // --- FUNCIÓN AUXILIAR PARA GUARDAR DATOS AGREGADOS --- + private async Task GuardarDatosAgregadosAsync(EleccionesDbContext dbContext, int ambitoId, int categoriaId, Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken) + { + await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); + + // 1. Actualizar EstadoRecuentoGeneral + var estadoGeneral = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken); + if (estadoGeneral == null) + { + estadoGeneral = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = ambitoId, CategoriaId = categoriaId }; + dbContext.EstadosRecuentosGenerales.Add(estadoGeneral); + } + estadoGeneral.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion); + estadoGeneral.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas; + estadoGeneral.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas; + estadoGeneral.MesasTotalizadasPorcentaje = resultadosDto.EstadoRecuento.MesasTotalizadasPorcentaje; + estadoGeneral.CantidadElectores = resultadosDto.EstadoRecuento.CantidadElectores; + estadoGeneral.CantidadVotantes = resultadosDto.EstadoRecuento.CantidadVotantes; + estadoGeneral.ParticipacionPorcentaje = resultadosDto.EstadoRecuento.ParticipacionPorcentaje; + + // 2. Actualizar ResumenesVotos + if (resultadosDto.ValoresTotalizadosPositivos is { Count: > 0 } nuevosVotos) + { + await dbContext.ResumenesVotos + .Where(rv => rv.AmbitoGeograficoId == ambitoId && rv.CategoriaId == categoriaId) + .ExecuteDeleteAsync(stoppingToken); + + foreach (var voto in nuevosVotos) + { + dbContext.ResumenesVotos.Add(new ResumenVoto + { + EleccionId = EleccionId, + AmbitoGeograficoId = ambitoId, + CategoriaId = categoriaId, + AgrupacionPoliticaId = voto.IdAgrupacion, + Votos = voto.Votos, + VotosPorcentaje = voto.VotosPorcentaje + }); + } + } + + await dbContext.SaveChangesAsync(stoppingToken); + await transaction.CommitAsync(stoppingToken); + } + + private async Task ActualizarResultadosDeProvincia(string authToken, string distritoId, int categoriaId) { try { using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var municipiosASondear = await dbContext.AmbitosGeograficos + var municipiosDeLaProvincia = await dbContext.AmbitosGeograficos .AsNoTracking() - .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) - .ToListAsync(stoppingToken); + .Where(a => a.NivelId == 30 && a.DistritoId == distritoId && a.SeccionId != null) + .ToListAsync(); - var todasLasCategorias = await dbContext.CategoriasElectorales - .AsNoTracking() - .ToListAsync(stoppingToken); + _logger.LogInformation("Iniciando descarga detallada para {Count} municipios del distrito {DistritoId}...", municipiosDeLaProvincia.Count, distritoId); - if (!municipiosASondear.Any() || !todasLasCategorias.Any()) + foreach (var municipio in municipiosDeLaProvincia) { - _logger.LogWarning("No se encontraron Partidos (NivelId 30) o Categorías para sondear resultados."); - return; - } - - _logger.LogInformation("Iniciando sondeo de resultados para {m} municipios y {c} categorías...", municipiosASondear.Count, todasLasCategorias.Count); - - foreach (var municipio in municipiosASondear) - { - if (stoppingToken.IsCancellationRequested) break; - - var tareasCategoria = todasLasCategorias.Select(async categoria => + var resultados = await _apiService.GetResultadosAsync(authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoriaId); + if (resultados != null) { - var resultados = await _apiService.GetResultadosAsync(authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoria.Id); - - if (resultados != null) - { - using var innerScope = _serviceProvider.CreateScope(); - var innerDbContext = innerScope.ServiceProvider.GetRequiredService(); - - // --- LLAMADA CORRECTA --- - await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken); - } - }); - - await Task.WhenAll(tareasCategoria); + await GuardarResultadosDeAmbitoAsync(dbContext, municipio.Id, categoriaId, resultados, CancellationToken.None); + } } + + _logger.LogInformation("Descarga detallada para el distrito {DistritoId} completada.", distritoId); } catch (Exception ex) { - _logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales."); + _logger.LogError(ex, "Error en la tarea de actualización de resultados para el distrito {DistritoId}.", distritoId); } } private async Task GuardarResultadosDeAmbitoAsync( - EleccionesDbContext dbContext, int ambitoId, int categoriaId, - Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken) + EleccionesDbContext dbContext, int ambitoId, int categoriaId, + Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken) { var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken); @@ -394,7 +218,7 @@ public class CriticalDataWorker : BackgroundService dbContext.EstadosRecuentos.Add(estadoRecuento); } - estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime(); + estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion); estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas; estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas; estadoRecuento.MesasTotalizadasPorcentaje = resultadosDto.EstadoRecuento.MesasTotalizadasPorcentaje; @@ -470,211 +294,4 @@ public class CriticalDataWorker : BackgroundService _logger.LogError(ex, "DbUpdateException al guardar resultados para AmbitoId {ambitoId} y CategoriaId {categoriaId}", ambitoId, categoriaId); } } - - /// - /// Obtiene y actualiza el resumen de votos y el estado del recuento a nivel provincial para CADA categoría. - /// Este método itera sobre todas las provincias y categorías, obteniendo sus resultados consolidados - /// y guardándolos en las tablas 'ResumenesVotos' y 'EstadosRecuentosGenerales'. - /// - private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken) - { - try - { - _logger.LogInformation("Iniciando sondeo de Resúmenes Provinciales..."); - - using var scope = _serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - var provinciasASondear = await dbContext.AmbitosGeograficos - .AsNoTracking() - .Where(a => a.NivelId == 10 && a.DistritoId != null) - .ToListAsync(stoppingToken); - - var todasLasCategorias = await dbContext.CategoriasElectorales - .AsNoTracking() - .ToListAsync(stoppingToken); - - if (!provinciasASondear.Any() || !todasLasCategorias.Any()) - { - _logger.LogWarning("No se encontraron Provincias o Categorías para sondear resúmenes."); - return; - } - - foreach (var provincia in provinciasASondear) - { - if (stoppingToken.IsCancellationRequested) break; - - foreach (var categoria in todasLasCategorias) - { - if (stoppingToken.IsCancellationRequested) break; - - // Usamos GetResultados sin seccionId/municipioId para obtener el resumen del distrito. - var resultadosDto = await _apiService.GetResultadosAsync(authToken, provincia.DistritoId!, null, null, categoria.Id); - - if (resultadosDto?.ValoresTotalizadosPositivos is { Count: > 0 } nuevosVotos) - { - // Usamos una transacción para asegurar que el borrado y la inserción sean atómicos. - await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); - - // A. Borrar los resúmenes viejos SOLO para esta provincia y categoría. - await dbContext.ResumenesVotos - .Where(rv => rv.AmbitoGeograficoId == provincia.Id && rv.CategoriaId == categoria.Id) - .ExecuteDeleteAsync(stoppingToken); - - // B. Añadir los nuevos resúmenes. - foreach (var voto in nuevosVotos) - { - dbContext.ResumenesVotos.Add(new ResumenVoto - { - EleccionId = EleccionId, - AmbitoGeograficoId = provincia.Id, - CategoriaId = categoria.Id, - AgrupacionPoliticaId = voto.IdAgrupacion, - Votos = voto.Votos, - VotosPorcentaje = voto.VotosPorcentaje - }); - } - - // C. Guardar los cambios en la tabla ResumenesVotos. - await dbContext.SaveChangesAsync(stoppingToken); - - // No es necesario actualizar EstadosRecuentosGenerales aquí, - // ya que el método SondearEstadoRecuentoGeneralAsync se encarga de eso - // de forma más específica y eficiente. - - await transaction.CommitAsync(stoppingToken); - } - } // Fin bucle categorías - } // Fin bucle provincias - - _logger.LogInformation("Sondeo de Resúmenes Provinciales completado."); - } - catch (OperationCanceledException) - { - _logger.LogInformation("Sondeo de resúmenes provinciales cancelado."); - } - catch (Exception ex) - { - _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Resúmenes Provinciales."); - } - } - - /// - /// Obtiene y actualiza el estado general del recuento a nivel provincial para CADA categoría electoral. - /// Esta versión es robusta: consulta dinámicamente las categorías, usa la clave primaria compuesta - /// de la base de datos y guarda todos los cambios en una única transacción al final. - /// - /// El token de autenticación válido para la sesión. - /// El token de cancelación para detener la operación. - private async Task SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken) - { - try - { - using var scope = _serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - var provinciasASondear = await dbContext.AmbitosGeograficos - .AsNoTracking() - .Where(a => a.NivelId == 10 && a.DistritoId != null) - .ToListAsync(stoppingToken); - - // Busca NivelId 1 (País) o 0 como fallback. - var ambitoNacional = await dbContext.AmbitosGeograficos - .AsNoTracking() - .FirstOrDefaultAsync(a => a.NivelId == 1 || a.NivelId == 0, stoppingToken); - - var categoriasParaSondear = await dbContext.CategoriasElectorales - .AsNoTracking() - .ToListAsync(stoppingToken); - - if (!provinciasASondear.Any() || !categoriasParaSondear.Any()) - { - _logger.LogWarning("No se encontraron Provincias o Categorías para sondear estado general."); - return; - } - - _logger.LogInformation("Iniciando sondeo de Estado Recuento General para {provCount} provincias, el total nacional y {catCount} categorías...", provinciasASondear.Count, categoriasParaSondear.Count); - - // Sondeo a nivel provincial - foreach (var provincia in provinciasASondear) - { - if (stoppingToken.IsCancellationRequested) break; - 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 }, stoppingToken); - if (registroDb == null) - { - registroDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = provincia.Id, CategoriaId = categoria.Id }; - dbContext.EstadosRecuentosGenerales.Add(registroDb); - } - registroDb.FechaTotalizacion = DateTime.UtcNow; - registroDb.MesasEsperadas = estadoDto.MesasEsperadas; - registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas; - registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje; - registroDb.CantidadElectores = estadoDto.CantidadElectores; - registroDb.CantidadVotantes = estadoDto.CantidadVotantes; - registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje; - } - } - } - - // Bloque para el sondeo a nivel nacional - if (ambitoNacional != null && !stoppingToken.IsCancellationRequested) - { - _logger.LogInformation("Sondeando totales a nivel Nacional (Ambito ID: {ambitoId})...", ambitoNacional.Id); - foreach (var categoria in categoriasParaSondear) - { - if (stoppingToken.IsCancellationRequested) break; - - var estadoNacionalDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, "", categoria.Id); - - if (estadoNacionalDto != null) - { - var registroNacionalDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { ambitoNacional.Id, categoria.Id }, stoppingToken); - if (registroNacionalDb == null) - { - registroNacionalDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = ambitoNacional.Id, CategoriaId = categoria.Id }; - dbContext.EstadosRecuentosGenerales.Add(registroNacionalDb); - } - registroNacionalDb.FechaTotalizacion = DateTime.UtcNow; - registroNacionalDb.MesasEsperadas = estadoNacionalDto.MesasEsperadas; - registroNacionalDb.MesasTotalizadas = estadoNacionalDto.MesasTotalizadas; - registroNacionalDb.MesasTotalizadasPorcentaje = estadoNacionalDto.MesasTotalizadasPorcentaje; - registroNacionalDb.CantidadElectores = estadoNacionalDto.CantidadElectores; - registroNacionalDb.CantidadVotantes = estadoNacionalDto.CantidadVotantes; - registroNacionalDb.ParticipacionPorcentaje = estadoNacionalDto.ParticipacionPorcentaje; - _logger.LogInformation("Datos nacionales para categoría '{catNombre}' actualizados.", categoria.Nombre); - } - } - } - else if (ambitoNacional == null) - { - _logger.LogWarning("No se encontró el ámbito geográfico para el Nivel Nacional (NivelId 1 o 0). No se pueden capturar los totales del país."); - } - - // Guardar todos los cambios - if (dbContext.ChangeTracker.HasChanges()) - { - await dbContext.SaveChangesAsync(stoppingToken); - _logger.LogInformation("Sondeo de Estado Recuento General completado. Se han guardado los cambios en la base de datos."); - } - else - { - _logger.LogInformation("Sondeo de Estado Recuento General completado. No se detectaron cambios."); - } - } - catch (OperationCanceledException) - { - _logger.LogInformation("Sondeo de Estado Recuento General cancelado."); - } - catch (Exception ex) - { - _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General."); - } - } -} +} \ No newline at end of file diff --git a/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs b/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs index 27ce044..3582405 100644 --- a/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs +++ b/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs @@ -1,4 +1,5 @@ -//Elecciones.Worker/LowPriorityDataWorker.cs +// src/Elecciones.Worker/LowPriorityDataWorker.cs + using Elecciones.Database; using Elecciones.Database.Entities; using Elecciones.Infrastructure.Services; @@ -15,15 +16,12 @@ public class LowPriorityDataWorker : BackgroundService private readonly IElectoralApiService _apiService; private readonly WorkerConfigService _configService; - // Una variable para rastrear la tarea de telegramas, si está en ejecución. - private Task? _telegramasTask; - public LowPriorityDataWorker( - ILogger logger, - SharedTokenService tokenService, - IServiceProvider serviceProvider, - IElectoralApiService apiService, - WorkerConfigService configService) + ILogger logger, + SharedTokenService tokenService, + IServiceProvider serviceProvider, + IElectoralApiService apiService, + WorkerConfigService configService) { _logger = logger; _tokenService = tokenService; @@ -34,14 +32,20 @@ public class LowPriorityDataWorker : BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation("Worker de Baja Prioridad iniciado."); + _logger.LogInformation("Worker de Baja Prioridad (Proyecciones) iniciado."); - // La sincronización inicial sigue siendo un paso de bloqueo, es necesario. + // Sincronización inicial de catálogos (solo se ejecuta una vez) await SincronizarCatalogosMaestrosAsync(stoppingToken); while (!stoppingToken.IsCancellationRequested) { - _logger.LogInformation("--- Iniciando Ciclo de Datos de Baja Prioridad ---"); + var settings = await _configService.GetSettingsAsync(); + if (!settings.BajasActivado) + { + _logger.LogInformation("El sondeo de proyecciones está desactivado. Esperando 5 minutos."); + await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); + continue; + } var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken); if (string.IsNullOrEmpty(authToken)) @@ -51,25 +55,10 @@ public class LowPriorityDataWorker : BackgroundService continue; } - var settings = await _configService.GetSettingsAsync(); + _logger.LogInformation("--- Iniciando Ciclo de Sondeo de Proyección de Bancas ---"); + await SondearProyeccionBancasAsync(authToken, stoppingToken); + _logger.LogInformation("--- Ciclo de Sondeo de Proyección de Bancas completado. ---"); - if (settings.Prioridad == "Telegramas" && settings.ResultadosActivado) - { - _logger.LogInformation("Ejecutando tareas de Resultados en baja prioridad."); - await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); - await SondearResultadosMunicipalesAsync(authToken, stoppingToken); - await SondearResumenProvincialAsync(authToken, stoppingToken); - } - else if (settings.Prioridad == "Resultados" && settings.BajasActivado) - { - _logger.LogInformation("Ejecutando tareas de Baja Prioridad en baja prioridad."); - await SondearProyeccionBancasAsync(authToken, stoppingToken); - //await SondearNuevosTelegramasAsync(authToken, stoppingToken); - } - else - { - _logger.LogInformation("Worker de baja prioridad inactivo según la configuración."); - } try { await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); @@ -81,349 +70,126 @@ public class LowPriorityDataWorker : BackgroundService } } - private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken) - { - try - { - using var scope = _serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - var municipiosASondear = await dbContext.AmbitosGeograficos - .AsNoTracking() - .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) - .ToListAsync(stoppingToken); - - var todasLasCategorias = await dbContext.CategoriasElectorales - .AsNoTracking() - .ToListAsync(stoppingToken); - - if (!municipiosASondear.Any() || !todasLasCategorias.Any()) - { - _logger.LogWarning("No se encontraron Partidos (NivelId 30) o Categorías para sondear resultados."); - return; - } - - _logger.LogInformation("Iniciando sondeo de resultados para {m} municipios y {c} categorías...", municipiosASondear.Count, todasLasCategorias.Count); - - foreach (var municipio in municipiosASondear) - { - if (stoppingToken.IsCancellationRequested) break; - - var tareasCategoria = todasLasCategorias.Select(async categoria => - { - var resultados = await _apiService.GetResultadosAsync(authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoria.Id); - - if (resultados != null) - { - using var innerScope = _serviceProvider.CreateScope(); - var innerDbContext = innerScope.ServiceProvider.GetRequiredService(); - - await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken); - } - }); - - await Task.WhenAll(tareasCategoria); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales."); - } - } - - private async Task GuardarResultadosDeAmbitoAsync( - EleccionesDbContext dbContext, int ambitoId, int categoriaId, - Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken) - { - var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken); - - if (estadoRecuento == null) - { - estadoRecuento = new EstadoRecuento { EleccionId = EleccionId, AmbitoGeograficoId = ambitoId, CategoriaId = categoriaId }; - 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; - estadoRecuento.VotosComando = resultadosDto.ValoresTotalizadosOtros.VotosComando; - estadoRecuento.VotosComandoPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosComandoPorcentaje; - estadoRecuento.VotosImpugnados = resultadosDto.ValoresTotalizadosOtros.VotosImpugnados; - estadoRecuento.VotosImpugnadosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosImpugnadosPorcentaje; - } - - foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos) - { - // PASO 1: VERIFICAR SI LA AGRUPACIÓN YA EXISTE EN NUESTRA BD - var agrupacion = await dbContext.AgrupacionesPoliticas.FindAsync(votoPositivoDto.IdAgrupacion); - - // PASO 2: SI NO EXISTE, LA CREAMOS "SOBRE LA MARCHA" - if (agrupacion == null) - { - _logger.LogWarning("Agrupación con ID {AgrupacionId} ('{Nombre}') no encontrada en el catálogo local. Creándola desde los datos de resultados.", - votoPositivoDto.IdAgrupacion, votoPositivoDto.NombreAgrupacion); - - agrupacion = new AgrupacionPolitica - { - Id = votoPositivoDto.IdAgrupacion, - Nombre = votoPositivoDto.NombreAgrupacion, - // El IdTelegrama puede ser nulo, usamos el operador '??' para asignar un string vacío si es así. - IdTelegrama = votoPositivoDto.IdAgrupacionTelegrama ?? string.Empty - }; - await dbContext.AgrupacionesPoliticas.AddAsync(agrupacion, stoppingToken); - // No es necesario llamar a SaveChangesAsync aquí, se hará al final. - } - - // PASO 3: CONTINUAR CON LA LÓGICA DE GUARDADO DEL VOTO - var resultadoVoto = await dbContext.ResultadosVotos.FirstOrDefaultAsync( - rv => rv.AmbitoGeograficoId == ambitoId && - rv.CategoriaId == categoriaId && - rv.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion, - stoppingToken - ); - - if (resultadoVoto == null) - { - resultadoVoto = new ResultadoVoto - { - EleccionId = EleccionId, - 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); - } - } - /// - /// Obtiene y actualiza el resumen de votos y el estado del recuento a nivel provincial para CADA categoría. - /// Este método itera sobre todas las provincias y categorías, obteniendo sus resultados consolidados - /// y guardándolos en las tablas 'ResumenesVotos' y 'EstadosRecuentosGenerales'. - /// - private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken) - { - try - { - _logger.LogInformation("Iniciando sondeo de Resúmenes Provinciales..."); - - using var scope = _serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - var provinciasASondear = await dbContext.AmbitosGeograficos - .AsNoTracking() - .Where(a => a.NivelId == 10 && a.DistritoId != null) - .ToListAsync(stoppingToken); - - var todasLasCategorias = await dbContext.CategoriasElectorales - .AsNoTracking() - .ToListAsync(stoppingToken); - - if (!provinciasASondear.Any() || !todasLasCategorias.Any()) - { - _logger.LogWarning("No se encontraron Provincias o Categorías para sondear resúmenes."); - return; - } - - foreach (var provincia in provinciasASondear) - { - if (stoppingToken.IsCancellationRequested) break; - - foreach (var categoria in todasLasCategorias) - { - if (stoppingToken.IsCancellationRequested) break; - - // Usamos GetResultados sin seccionId/municipioId para obtener el resumen del distrito. - var resultadosDto = await _apiService.GetResultadosAsync(authToken, provincia.DistritoId!, null, null, categoria.Id); - - if (resultadosDto?.ValoresTotalizadosPositivos is { Count: > 0 } nuevosVotos) - { - // Usamos una transacción para asegurar que el borrado y la inserción sean atómicos. - await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); - - // A. Borrar los resúmenes viejos SOLO para esta provincia y categoría. - await dbContext.ResumenesVotos - .Where(rv => rv.AmbitoGeograficoId == provincia.Id && rv.CategoriaId == categoria.Id) - .ExecuteDeleteAsync(stoppingToken); - - // B. Añadir los nuevos resúmenes. - foreach (var voto in nuevosVotos) - { - dbContext.ResumenesVotos.Add(new ResumenVoto - { - EleccionId = EleccionId, - AmbitoGeograficoId = provincia.Id, - CategoriaId = categoria.Id, - AgrupacionPoliticaId = voto.IdAgrupacion, - Votos = voto.Votos, - VotosPorcentaje = voto.VotosPorcentaje - }); - } - - // C. Guardar los cambios en la tabla ResumenesVotos. - await dbContext.SaveChangesAsync(stoppingToken); - - // No es necesario actualizar EstadosRecuentosGenerales aquí, - // ya que el método SondearEstadoRecuentoGeneralAsync se encarga de eso - // de forma más específica y eficiente. - - await transaction.CommitAsync(stoppingToken); - } - } // Fin bucle categorías - } // Fin bucle provincias - - _logger.LogInformation("Sondeo de Resúmenes Provinciales completado."); - } - catch (OperationCanceledException) - { - _logger.LogInformation("Sondeo de resúmenes provinciales cancelado."); - } - catch (Exception ex) - { - _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Resúmenes Provinciales."); - } - } - - /// - /// Obtiene y actualiza el estado general del recuento a nivel provincial para CADA categoría electoral. - /// Esta versión es robusta: consulta dinámicamente las categorías, usa la clave primaria compuesta - /// de la base de datos y guarda todos los cambios en una única transacción al final. + /// Sondea la proyección de bancas a nivel Provincial y por Sección Electoral. + /// Esta versión es completamente robusta: maneja respuestas de API vacías o con fechas mal formadas, + /// guarda la CategoriaId y usa una transacción atómica para la escritura en base de datos. /// /// El token de autenticación válido para la sesión. /// El token de cancelación para detener la operación. - private async Task SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken) + private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) { try { using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var provinciasASondear = await dbContext.AmbitosGeograficos + var categoriasDeBancas = await dbContext.CategoriasElectorales + .AsNoTracking() + .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) + .ToListAsync(stoppingToken); + + var ambitosASondear = await dbContext.AmbitosGeograficos .AsNoTracking() .Where(a => a.NivelId == 10 && a.DistritoId != null) .ToListAsync(stoppingToken); - // Busca NivelId 1 (País) o 0 como fallback. - var ambitoNacional = await dbContext.AmbitosGeograficos - .AsNoTracking() - .FirstOrDefaultAsync(a => a.NivelId == 1 || a.NivelId == 0, stoppingToken); - - var categoriasParaSondear = await dbContext.CategoriasElectorales - .AsNoTracking() - .ToListAsync(stoppingToken); - - if (!provinciasASondear.Any() || !categoriasParaSondear.Any()) + if (!categoriasDeBancas.Any() || !ambitosASondear.Any()) { - _logger.LogWarning("No se encontraron Provincias o Categorías para sondear estado general."); + _logger.LogWarning("No se encontraron categorías de bancas o ámbitos provinciales en la BD. Omitiendo sondeo de bancas."); return; } - _logger.LogInformation("Iniciando sondeo de Estado Recuento General para {provCount} provincias, el total nacional y {catCount} categorías...", provinciasASondear.Count, categoriasParaSondear.Count); + _logger.LogInformation("Iniciando sondeo de Bancas a nivel Provincial para {count} provincias...", ambitosASondear.Count); - // Sondeo a nivel provincial - foreach (var provincia in provinciasASondear) + var todasLasProyecciones = new List(); + bool hasReceivedAnyNewData = false; + var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); + + foreach (var ambito in ambitosASondear) { if (stoppingToken.IsCancellationRequested) break; - foreach (var categoria in categoriasParaSondear) + foreach (var categoria in categoriasDeBancas) { if (stoppingToken.IsCancellationRequested) break; - var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id); - if (estadoDto != null) + var repartoBancasDto = await _apiService.GetBancasAsync(authToken, ambito.DistritoId!, ambito.SeccionProvincialId, categoria.Id); + if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) { - var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { provincia.Id, categoria.Id }, stoppingToken); - if (registroDb == null) + hasReceivedAnyNewData = true; + DateTime fechaTotalizacion; + if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) { - registroDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = provincia.Id, CategoriaId = categoria.Id }; - dbContext.EstadosRecuentosGenerales.Add(registroDb); + fechaTotalizacion = DateTime.Now; + } + else + { + fechaTotalizacion = parsedDate; + } + + foreach (var banca in bancas) + { + if (!agrupacionesEnDb.ContainsKey(banca.IdAgrupacion)) + { + var nuevaAgrupacion = new AgrupacionPolitica + { + Id = banca.IdAgrupacion, + Nombre = banca.NombreAgrupacion, + IdTelegrama = banca.IdAgrupacionTelegrama ?? string.Empty + }; + await dbContext.AgrupacionesPoliticas.AddAsync(nuevaAgrupacion, stoppingToken); + agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); + } + todasLasProyecciones.Add(new ProyeccionBanca + { + EleccionId = EleccionId, + AmbitoGeograficoId = ambito.Id, + AgrupacionPoliticaId = banca.IdAgrupacion, + NroBancas = banca.NroBancas, + CategoriaId = categoria.Id, + FechaTotalizacion = fechaTotalizacion + }); } - registroDb.FechaTotalizacion = DateTime.UtcNow; - registroDb.MesasEsperadas = estadoDto.MesasEsperadas; - registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas; - registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje; - registroDb.CantidadElectores = estadoDto.CantidadElectores; - registroDb.CantidadVotantes = estadoDto.CantidadVotantes; - registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje; } } } - // Bloque para el sondeo a nivel nacional - if (ambitoNacional != null && !stoppingToken.IsCancellationRequested) + if (hasReceivedAnyNewData) { - _logger.LogInformation("Sondeando totales a nivel Nacional (Ambito ID: {ambitoId})...", ambitoNacional.Id); - foreach (var categoria in categoriasParaSondear) + _logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos..."); + await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); + + // Guardar nuevas agrupaciones si las hay + if (dbContext.ChangeTracker.HasChanges()) { - if (stoppingToken.IsCancellationRequested) break; - - var estadoNacionalDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, "", categoria.Id); - - if (estadoNacionalDto != null) - { - var registroNacionalDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { ambitoNacional.Id, categoria.Id }, stoppingToken); - if (registroNacionalDb == null) - { - registroNacionalDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = ambitoNacional.Id, CategoriaId = categoria.Id }; - dbContext.EstadosRecuentosGenerales.Add(registroNacionalDb); - } - registroNacionalDb.FechaTotalizacion = DateTime.UtcNow; - registroNacionalDb.MesasEsperadas = estadoNacionalDto.MesasEsperadas; - registroNacionalDb.MesasTotalizadas = estadoNacionalDto.MesasTotalizadas; - registroNacionalDb.MesasTotalizadasPorcentaje = estadoNacionalDto.MesasTotalizadasPorcentaje; - registroNacionalDb.CantidadElectores = estadoNacionalDto.CantidadElectores; - registroNacionalDb.CantidadVotantes = estadoNacionalDto.CantidadVotantes; - registroNacionalDb.ParticipacionPorcentaje = estadoNacionalDto.ParticipacionPorcentaje; - _logger.LogInformation("Datos nacionales para categoría '{catNombre}' actualizados.", categoria.Nombre); - } + await dbContext.SaveChangesAsync(stoppingToken); } - } - else if (ambitoNacional == null) - { - _logger.LogWarning("No se encontró el ámbito geográfico para el Nivel Nacional (NivelId 1 o 0). No se pueden capturar los totales del país."); - } - // Guardar todos los cambios - if (dbContext.ChangeTracker.HasChanges()) - { + // --- LÍNEA CORREGIDA --- + // Se reemplaza ExecuteSqlRawAsync por ExecuteDeleteAsync, que es type-safe + // y maneja correctamente el CancellationToken. + await dbContext.ProyeccionesBancas + .Where(p => p.EleccionId == EleccionId) + .ExecuteDeleteAsync(stoppingToken); + // --- FIN DE LA CORRECCIÓN --- + + await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken); await dbContext.SaveChangesAsync(stoppingToken); - _logger.LogInformation("Sondeo de Estado Recuento General completado. Se han guardado los cambios en la base de datos."); + await transaction.CommitAsync(stoppingToken); + _logger.LogInformation("La tabla de proyecciones ha sido actualizada con {count} registros.", todasLasProyecciones.Count); } else { - _logger.LogInformation("Sondeo de Estado Recuento General completado. No se detectaron cambios."); + _logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos, la tabla no fue modificada."); } } catch (OperationCanceledException) { - _logger.LogInformation("Sondeo de Estado Recuento General cancelado."); + _logger.LogInformation("Sondeo de bancas cancelado."); } catch (Exception ex) { - _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General."); + _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas."); } } @@ -552,245 +318,4 @@ public class LowPriorityDataWorker : BackgroundService _logger.LogError(ex, "Ocurrió un error CRÍTICO durante la sincronización de catálogos."); } } - - /// - /// Sondea la proyección de bancas a nivel Provincial y por Sección Electoral. - /// Esta versión es completamente robusta: maneja respuestas de API vacías o con fechas mal formadas, - /// guarda la CategoriaId y usa una transacción atómica para la escritura en base de datos. - /// - /// El token de autenticación válido para la sesión. - /// El token de cancelación para detener la operación. - private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) - { - try - { - using var scope = _serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - var categoriasDeBancas = await dbContext.CategoriasElectorales - .AsNoTracking() - .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) - .ToListAsync(stoppingToken); - - var ambitosASondear = await dbContext.AmbitosGeograficos - .AsNoTracking() - .Where(a => a.NivelId == 10 && a.DistritoId != null) - .ToListAsync(stoppingToken); - - if (!categoriasDeBancas.Any() || !ambitosASondear.Any()) - { - _logger.LogWarning("No se encontraron categorías de bancas o ámbitos provinciales en la BD. Omitiendo sondeo de bancas."); - return; - } - - _logger.LogInformation("Iniciando sondeo de Bancas a nivel Provincial para {count} provincias...", ambitosASondear.Count); - - var todasLasProyecciones = new List(); - bool hasReceivedAnyNewData = false; - var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); - - foreach (var ambito in ambitosASondear) - { - if (stoppingToken.IsCancellationRequested) break; - foreach (var categoria in categoriasDeBancas) - { - if (stoppingToken.IsCancellationRequested) break; - - var repartoBancasDto = await _apiService.GetBancasAsync(authToken, ambito.DistritoId!, ambito.SeccionProvincialId, categoria.Id); - - if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) - { - hasReceivedAnyNewData = true; - DateTime fechaTotalizacion; - if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) - { - _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); - fechaTotalizacion = DateTime.UtcNow; - } - else - { - fechaTotalizacion = parsedDate.ToUniversalTime(); - } - - foreach (var banca in bancas) - { - if (!agrupacionesEnDb.ContainsKey(banca.IdAgrupacion)) - { - _logger.LogWarning("Agrupación con ID {AgrupacionId} ('{Nombre}') no encontrada. Creándola desde los datos de bancas.", banca.IdAgrupacion, banca.NombreAgrupacion); - var nuevaAgrupacion = new AgrupacionPolitica - { - Id = banca.IdAgrupacion, - Nombre = banca.NombreAgrupacion, - IdTelegrama = banca.IdAgrupacionTelegrama ?? string.Empty - }; - await dbContext.AgrupacionesPoliticas.AddAsync(nuevaAgrupacion, stoppingToken); - agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); - } - - todasLasProyecciones.Add(new ProyeccionBanca - { - EleccionId = EleccionId, - AmbitoGeograficoId = ambito.Id, - AgrupacionPoliticaId = banca.IdAgrupacion, - NroBancas = banca.NroBancas, - CategoriaId = categoria.Id, - FechaTotalizacion = fechaTotalizacion - }); - } - } - } - } - - if (hasReceivedAnyNewData) - { - _logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos..."); - await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); - await dbContext.SaveChangesAsync(stoppingToken); // Guardar nuevas agrupaciones si las hay - await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas WHERE EleccionId = {0}", EleccionId, stoppingToken); // CORRECCIÓN: Borrar solo para la elección actual - await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken); - await dbContext.SaveChangesAsync(stoppingToken); - await transaction.CommitAsync(stoppingToken); - _logger.LogInformation("La tabla de proyecciones ha sido actualizada con {count} registros.", todasLasProyecciones.Count); - } - else - { - _logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos, la tabla no fue modificada."); - } - } - catch (OperationCanceledException) - { - _logger.LogInformation("Sondeo de bancas cancelado."); - } - catch (Exception ex) - { - _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas."); - } - } - - /// - /// Busca y descarga nuevos telegramas de forma masiva y concurrente. - /// Este método crea una lista de todas las combinaciones de Partido/Categoría, - /// las consulta a la API con un grado de paralelismo controlado, y cada tarea concurrente - /// maneja su propia lógica de descarga y guardado en la base de datos. - /// - /// El token de autenticación válido para la sesión. - /// El token de cancelación para detener la operación. - private async Task SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken) - { - try - { - _logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas (modo de bajo perfil) ---"); - - using var scope = _serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - // La obtención de partidos y categorías no cambia - var partidos = await dbContext.AmbitosGeograficos.AsNoTracking() - .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) - .ToListAsync(stoppingToken); - var categorias = await dbContext.CategoriasElectorales.AsNoTracking().ToListAsync(stoppingToken); - - if (!partidos.Any() || !categorias.Any()) return; - - foreach (var partido in partidos) - { - foreach (var categoria in categorias) - { - if (stoppingToken.IsCancellationRequested) return; - - var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id); - - if (listaTelegramasApi is { Count: > 0 }) - { - // Creamos el DbContext para la operación de guardado - using var innerScope = _serviceProvider.CreateScope(); - var innerDbContext = innerScope.ServiceProvider.GetRequiredService(); - - var idsYaEnDb = await innerDbContext.Telegramas - .Where(t => listaTelegramasApi.Contains(t.Id)) - .Select(t => t.Id).ToListAsync(stoppingToken); - - var nuevosTelegramasIds = listaTelegramasApi.Except(idsYaEnDb).ToList(); - - if (nuevosTelegramasIds.Any()) - { - _logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando...", nuevosTelegramasIds.Count, partido.Nombre, categoria.Nombre); - - var originalTimeout = innerDbContext.Database.GetCommandTimeout(); - try - { - innerDbContext.Database.SetCommandTimeout(180); - _logger.LogDebug("Timeout de BD aumentado a 180s para descarga de telegramas."); - - int contadorLote = 0; - const int tamanoLote = 100; - - foreach (var mesaId in nuevosTelegramasIds) - { - if (stoppingToken.IsCancellationRequested) return; - - var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); - if (telegramaFile != null) - { - var ambitoMesa = await innerDbContext.AmbitosGeograficos.AsNoTracking() - .FirstOrDefaultAsync(a => a.MesaId == mesaId, stoppingToken); - - if (ambitoMesa != null) - { - var nuevoTelegrama = new Telegrama - { - EleccionId = EleccionId, - Id = telegramaFile.NombreArchivo, - AmbitoGeograficoId = ambitoMesa.Id, - ContenidoBase64 = telegramaFile.Imagen, - FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), - FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() - }; - await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken); - contadorLote++; - } - else - { - _logger.LogWarning("No se encontró un ámbito geográfico para la mesa con MesaId {MesaId}. El telegrama no será guardado.", mesaId); - } - } - await Task.Delay(250, stoppingToken); - - if (contadorLote >= tamanoLote) - { - await innerDbContext.SaveChangesAsync(stoppingToken); - _logger.LogInformation("Guardado un lote de {count} telegramas.", contadorLote); - contadorLote = 0; - } - } - - if (contadorLote > 0) - { - await innerDbContext.SaveChangesAsync(stoppingToken); - _logger.LogInformation("Guardado el último lote de {count} telegramas.", contadorLote); - } - } - finally - { - innerDbContext.Database.SetCommandTimeout(originalTimeout); - _logger.LogDebug("Timeout de BD restaurado a su valor original ({timeout}s).", originalTimeout); - } - } // Fin del if (nuevosTelegramasIds.Any()) - - // Movemos el delay aquí para que solo se ejecute si hubo telegramas en la respuesta de la API - await Task.Delay(100, stoppingToken); - } // Fin del if (listaTelegramasApi is not null) - } - } - _logger.LogInformation("Sondeo de Telegramas completado."); - } - catch (OperationCanceledException) - { - _logger.LogInformation("Sondeo de telegramas cancelado."); - } - catch (Exception ex) - { - _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Telegramas."); - } - } } \ No newline at end of file