Fix Mapa Municipios - Limpieza y Optimización de Workers
This commit is contained in:
		| @@ -324,7 +324,6 @@ export const getHomeResumenNacional = async (eleccionId: number, categoriaId: nu | |||||||
|     eleccionId: eleccionId.toString(), |     eleccionId: eleccionId.toString(), | ||||||
|     categoriaId: categoriaId.toString(), |     categoriaId: categoriaId.toString(), | ||||||
|   }); |   }); | ||||||
|   // Apunta al nuevo endpoint que creamos |  | ||||||
|   const url = `/elecciones/home-resumen-nacional?${queryParams.toString()}`; |   const url = `/elecciones/home-resumen-nacional?${queryParams.toString()}`; | ||||||
|   const { data } = await apiClient.get(url); |   const { data } = await apiClient.get(url); | ||||||
|   return data; |   return data; | ||||||
|   | |||||||
| @@ -1011,24 +1011,29 @@ public class ResultadosController : ControllerBase | |||||||
|     [HttpGet("panel/{ambitoId?}")] |     [HttpGet("panel/{ambitoId?}")] | ||||||
|     public async Task<IActionResult> GetPanelElectoral(int eleccionId, string? ambitoId, [FromQuery] int categoriaId) |     public async Task<IActionResult> GetPanelElectoral(int eleccionId, string? ambitoId, [FromQuery] int categoriaId) | ||||||
|     { |     { | ||||||
|  |         // Caso 1: Sin ID -> Vista Nacional | ||||||
|         if (string.IsNullOrEmpty(ambitoId)) |         if (string.IsNullOrEmpty(ambitoId)) | ||||||
|         { |         { | ||||||
|             // CASO 1: No hay ID -> Vista Nacional |  | ||||||
|             return await GetPanelNacional(eleccionId, categoriaId); |             return await GetPanelNacional(eleccionId, categoriaId); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // CASO 2: El ID es un número (y no un string corto como "02") -> Vista Municipal |         // Intenta interpretar el ambitoId como un ID numérico de municipio. | ||||||
|         // La condición clave es que los IDs de distrito son cortos. Los IDs de BD son más largos. |         if (int.TryParse(ambitoId, out int idNumerico)) | ||||||
|         // O simplemente, un ID de distrito nunca será un ID de municipio. |  | ||||||
|         if (int.TryParse(ambitoId, out int idNumerico) && ambitoId.Length > 2) |  | ||||||
|         { |         { | ||||||
|             return await GetPanelMunicipal(eleccionId, idNumerico, 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. | ||||||
|         else |             bool esMunicipio = await _dbContext.AmbitosGeograficos | ||||||
|         { |                 .AnyAsync(a => a.Id == idNumerico && a.NivelId == 30); | ||||||
|             // CASO 3: El ID es un string corto como "02" o "06" -> Vista Provincial |  | ||||||
|             return await GetPanelProvincial(eleccionId, ambitoId, categoriaId); |             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<IActionResult> GetPanelMunicipal(int eleccionId, int ambitoId, int categoriaId) |     private async Task<IActionResult> 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 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 todosLosLogos = await _dbContext.LogosAgrupacionesCategorias.AsNoTracking().Where(l => l.EleccionId == eleccionId || l.EleccionId == 0).ToListAsync(); | ||||||
|  |  | ||||||
|  |  | ||||||
|         var resultadosAgregados = await _dbContext.ResultadosVotos.AsNoTracking() |         var resultadosAgregados = await _dbContext.ResultadosVotos.AsNoTracking() | ||||||
|             .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId) |             .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId) | ||||||
|             .GroupBy(r => r.AgrupacionPolitica) |             .GroupBy(r => r.AgrupacionPolitica) | ||||||
| @@ -1202,7 +1206,7 @@ public class ResultadosController : ControllerBase | |||||||
|                 Color = g.Agrupacion.Color, |                 Color = g.Agrupacion.Color, | ||||||
|                 Votos = g.TotalVotos, |                 Votos = g.TotalVotos, | ||||||
|                 Porcentaje = totalVotosNacional > 0 ? (g.TotalVotos / totalVotosNacional) * 100 : 0, |                 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 |                 LogoUrl = logoMatch?.LogoUrl | ||||||
|             }; |             }; | ||||||
|         }) |         }) | ||||||
| @@ -1210,7 +1214,7 @@ public class ResultadosController : ControllerBase | |||||||
|         .ToList(); |         .ToList(); | ||||||
|  |  | ||||||
|         var estadoRecuento = await _dbContext.EstadosRecuentosGenerales.AsNoTracking() |         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 |         var respuesta = new PanelElectoralDto | ||||||
|         { |         { | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ using System.Reflection; | |||||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")] | [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")] | ||||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | [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.AssemblyProductAttribute("Elecciones.Api")] | ||||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] | [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] | ||||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||||
|   | |||||||
| @@ -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":{}} | {"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":{}} | ||||||
| @@ -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":{}} | {"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":{}} | ||||||
| @@ -13,7 +13,7 @@ using System.Reflection; | |||||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")] | [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")] | ||||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | [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.AssemblyProductAttribute("Elecciones.Core")] | ||||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")] | [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")] | ||||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ using System.Reflection; | |||||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")] | [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")] | ||||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | [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.AssemblyProductAttribute("Elecciones.Database")] | ||||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")] | [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")] | ||||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ using System.Reflection; | |||||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")] | [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")] | ||||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | [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.AssemblyProductAttribute("Elecciones.Infrastructure")] | ||||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")] | [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")] | ||||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| //Elecciones.Worker/CriticalDataWorker.cs | // src/Elecciones.Worker/CriticalDataWorker.cs | ||||||
|  |  | ||||||
| using Elecciones.Database; | using Elecciones.Database; | ||||||
| using Elecciones.Database.Entities; | using Elecciones.Database.Entities; | ||||||
| using Elecciones.Infrastructure.Services; | using Elecciones.Infrastructure.Services; | ||||||
| @@ -15,6 +16,7 @@ public class CriticalDataWorker : BackgroundService | |||||||
|   private readonly IServiceProvider _serviceProvider; |   private readonly IServiceProvider _serviceProvider; | ||||||
|   private readonly IElectoralApiService _apiService; |   private readonly IElectoralApiService _apiService; | ||||||
|   private readonly WorkerConfigService _configService; |   private readonly WorkerConfigService _configService; | ||||||
|  |   private static readonly ConcurrentDictionary<string, DateTime> _lastUpdateTracker = new(); | ||||||
|  |  | ||||||
|   public CriticalDataWorker( |   public CriticalDataWorker( | ||||||
|       ILogger<CriticalDataWorker> logger, |       ILogger<CriticalDataWorker> logger, | ||||||
| @@ -32,20 +34,18 @@ public class CriticalDataWorker : BackgroundService | |||||||
|  |  | ||||||
|   protected override async Task ExecuteAsync(CancellationToken stoppingToken) |   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) |     while (!stoppingToken.IsCancellationRequested) | ||||||
|     { |     { | ||||||
|       var cicloInicio = DateTime.UtcNow; |       var settings = await _configService.GetSettingsAsync(); | ||||||
|       cicloContador++; |       if (!settings.ResultadosActivado) | ||||||
|       _logger.LogInformation("--- Iniciando Ciclo de Datos Críticos #{ciclo} ---", cicloContador); |       { | ||||||
|  |         _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); |       var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken); | ||||||
|       if (string.IsNullOrEmpty(authToken)) |       if (string.IsNullOrEmpty(authToken)) | ||||||
| @@ -55,336 +55,160 @@ public class CriticalDataWorker : BackgroundService | |||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       var settings = await _configService.GetSettingsAsync(); |       _logger.LogInformation("--- Iniciando Ciclo de Sondeo Inteligente ---"); | ||||||
|  |       await SondearEstadosYDispararActualizaciones(authToken, stoppingToken); | ||||||
|       if (settings.Prioridad == "Resultados" && settings.ResultadosActivado) |       _logger.LogInformation("--- Ciclo de Sondeo Inteligente completado. ---"); | ||||||
|       { |  | ||||||
|         _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; |  | ||||||
|  |  | ||||||
|       try |       try | ||||||
|       { |       { | ||||||
|         await Task.Delay(tiempoDeEspera, stoppingToken); |         await Task.Delay(TimeSpan.FromSeconds(45), stoppingToken); | ||||||
|       } |       } | ||||||
|       catch (TaskCanceledException) { break; } |       catch (TaskCanceledException) { break; } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /// <summary> |   private async Task SondearEstadosYDispararActualizaciones(string authToken, CancellationToken stoppingToken) | ||||||
|   /// 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. |  | ||||||
|   /// </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 SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) |  | ||||||
|   { |   { | ||||||
|     try |     try | ||||||
|     { |     { | ||||||
|       using var scope = _serviceProvider.CreateScope(); |       using var scope = _serviceProvider.CreateScope(); | ||||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|       var categoriasDeBancas = await dbContext.CategoriasElectorales |       var ambitosProvinciales = await dbContext.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 10 && a.DistritoId != null).ToListAsync(stoppingToken); | ||||||
|           .AsNoTracking() |       var ambitoNacional = await dbContext.AmbitosGeograficos.AsNoTracking().FirstOrDefaultAsync(a => a.NivelId == 1 || a.NivelId == 0, stoppingToken); | ||||||
|           .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) |       var categorias = await dbContext.CategoriasElectorales.AsNoTracking().ToListAsync(stoppingToken); | ||||||
|           .ToListAsync(stoppingToken); |  | ||||||
|  |  | ||||||
|       var ambitosASondear = await dbContext.AmbitosGeograficos |       if (!ambitosProvinciales.Any() || !categorias.Any()) return; | ||||||
|           .AsNoTracking() |  | ||||||
|           .Where(a => a.NivelId == 10 && a.DistritoId != null) |  | ||||||
|           .ToListAsync(stoppingToken); |  | ||||||
|  |  | ||||||
|       if (!categoriasDeBancas.Any() || !ambitosASondear.Any()) |       // --- PASO 1: SONDEO PROVINCIAL --- | ||||||
|       { |       foreach (var provincia in ambitosProvinciales) | ||||||
|         _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<ProyeccionBanca>(); |  | ||||||
|       bool hasReceivedAnyNewData = false; |  | ||||||
|       var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); |  | ||||||
|  |  | ||||||
|       foreach (var ambito in ambitosASondear) |  | ||||||
|       { |       { | ||||||
|         if (stoppingToken.IsCancellationRequested) break; |         if (stoppingToken.IsCancellationRequested) break; | ||||||
|         foreach (var categoria in categoriasDeBancas) |         foreach (var categoria in categorias) | ||||||
|         { |         { | ||||||
|           if (stoppingToken.IsCancellationRequested) break; |           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; |             var trackerKey = $"{provincia.Id}-{categoria.Id}"; | ||||||
|             DateTime fechaTotalizacion; |             _lastUpdateTracker.TryGetValue(trackerKey, out var ultimaFechaConocida); | ||||||
|             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 (fechaApi > ultimaFechaConocida) | ||||||
|             { |             { | ||||||
|               if (!agrupacionesEnDb.ContainsKey(banca.IdAgrupacion)) |               _logger.LogInformation("¡Cambio detectado para {Provincia} - {Categoria}! Actualizando detalles en segundo plano...", provincia.Nombre, categoria.Nombre); | ||||||
|               { |               _lastUpdateTracker[trackerKey] = fechaApi; | ||||||
|                 _logger.LogWarning("Agrupación con ID {AgrupacionId} ('{Nombre}') no encontrada. Creándola desde los datos de bancas.", banca.IdAgrupacion, banca.NombreAgrupacion); |               _ = Task.Run(() => ActualizarResultadosDeProvincia(authToken, provincia.DistritoId!, categoria.Id), stoppingToken); | ||||||
|                 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) |       // --- PASO 2: SONDEO NACIONAL --- | ||||||
|       { |       if (ambitoNacional != null && !stoppingToken.IsCancellationRequested) | ||||||
|         _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."); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /// <summary> |  | ||||||
|   /// 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. |  | ||||||
|   /// </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 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<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|       // 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) |  | ||||||
|       { |       { | ||||||
|  |         _logger.LogInformation("Sondeando totales a nivel Nacional..."); | ||||||
|         foreach (var categoria in categorias) |         foreach (var categoria in categorias) | ||||||
|         { |         { | ||||||
|           if (stoppingToken.IsCancellationRequested) return; |           if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |           var resultadosNacionalDto = await _apiService.GetResultadosAsync(authToken, "", null, null, categoria.Id); | ||||||
|           var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id); |           if (resultadosNacionalDto != null) | ||||||
|  |  | ||||||
|           if (listaTelegramasApi is { Count: > 0 }) |  | ||||||
|           { |           { | ||||||
|             // Creamos el DbContext para la operación de guardado |             await GuardarDatosAgregadosAsync(dbContext, ambitoNacional.Id, categoria.Id, resultadosNacionalDto, stoppingToken); | ||||||
|             using var innerScope = _serviceProvider.CreateScope(); |           } | ||||||
|             var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|             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) |     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 |     try | ||||||
|     { |     { | ||||||
|       using var scope = _serviceProvider.CreateScope(); |       using var scope = _serviceProvider.CreateScope(); | ||||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|       var municipiosASondear = await dbContext.AmbitosGeograficos |       var municipiosDeLaProvincia = await dbContext.AmbitosGeograficos | ||||||
|           .AsNoTracking() |           .AsNoTracking() | ||||||
|           .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) |           .Where(a => a.NivelId == 30 && a.DistritoId == distritoId && a.SeccionId != null) | ||||||
|           .ToListAsync(stoppingToken); |           .ToListAsync(); | ||||||
|  |  | ||||||
|       var todasLasCategorias = await dbContext.CategoriasElectorales |       _logger.LogInformation("Iniciando descarga detallada para {Count} municipios del distrito {DistritoId}...", municipiosDeLaProvincia.Count, distritoId); | ||||||
|           .AsNoTracking() |  | ||||||
|           .ToListAsync(stoppingToken); |  | ||||||
|  |  | ||||||
|       if (!municipiosASondear.Any() || !todasLasCategorias.Any()) |       foreach (var municipio in municipiosDeLaProvincia) | ||||||
|       { |       { | ||||||
|         _logger.LogWarning("No se encontraron Partidos (NivelId 30) o Categorías para sondear resultados."); |         var resultados = await _apiService.GetResultadosAsync(authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoriaId); | ||||||
|         return; |         if (resultados != null) | ||||||
|       } |  | ||||||
|  |  | ||||||
|       _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); |           await GuardarResultadosDeAmbitoAsync(dbContext, municipio.Id, categoriaId, resultados, CancellationToken.None); | ||||||
|  |         } | ||||||
|           if (resultados != null) |  | ||||||
|           { |  | ||||||
|             using var innerScope = _serviceProvider.CreateScope(); |  | ||||||
|             var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|             // --- LLAMADA CORRECTA --- |  | ||||||
|             await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken); |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         await Task.WhenAll(tareasCategoria); |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       _logger.LogInformation("Descarga detallada para el distrito {DistritoId} completada.", distritoId); | ||||||
|     } |     } | ||||||
|     catch (Exception ex) |     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( |   private async Task GuardarResultadosDeAmbitoAsync( | ||||||
|       EleccionesDbContext dbContext, int ambitoId, int categoriaId, |     EleccionesDbContext dbContext, int ambitoId, int categoriaId, | ||||||
|       Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken) |     Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken) | ||||||
|   { |   { | ||||||
|     var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken); |     var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken); | ||||||
|  |  | ||||||
| @@ -394,7 +218,7 @@ public class CriticalDataWorker : BackgroundService | |||||||
|       dbContext.EstadosRecuentos.Add(estadoRecuento); |       dbContext.EstadosRecuentos.Add(estadoRecuento); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime(); |     estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion); | ||||||
|     estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas; |     estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas; | ||||||
|     estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas; |     estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas; | ||||||
|     estadoRecuento.MesasTotalizadasPorcentaje = resultadosDto.EstadoRecuento.MesasTotalizadasPorcentaje; |     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); |       _logger.LogError(ex, "DbUpdateException al guardar resultados para AmbitoId {ambitoId} y CategoriaId {categoriaId}", ambitoId, categoriaId); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /// <summary> |  | ||||||
|   /// 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'. |  | ||||||
|   /// </summary> |  | ||||||
|   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<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|       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."); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /// <summary> |  | ||||||
|   /// 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. |  | ||||||
|   /// </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 SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken) |  | ||||||
|   { |  | ||||||
|     try |  | ||||||
|     { |  | ||||||
|       using var scope = _serviceProvider.CreateScope(); |  | ||||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|       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."); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| //Elecciones.Worker/LowPriorityDataWorker.cs | // src/Elecciones.Worker/LowPriorityDataWorker.cs | ||||||
|  |  | ||||||
| using Elecciones.Database; | using Elecciones.Database; | ||||||
| using Elecciones.Database.Entities; | using Elecciones.Database.Entities; | ||||||
| using Elecciones.Infrastructure.Services; | using Elecciones.Infrastructure.Services; | ||||||
| @@ -15,15 +16,12 @@ public class LowPriorityDataWorker : BackgroundService | |||||||
|   private readonly IElectoralApiService _apiService; |   private readonly IElectoralApiService _apiService; | ||||||
|   private readonly WorkerConfigService _configService; |   private readonly WorkerConfigService _configService; | ||||||
|  |  | ||||||
|   // Una variable para rastrear la tarea de telegramas, si está en ejecución. |  | ||||||
|   private Task? _telegramasTask; |  | ||||||
|  |  | ||||||
|   public LowPriorityDataWorker( |   public LowPriorityDataWorker( | ||||||
|   ILogger<LowPriorityDataWorker> logger, |       ILogger<LowPriorityDataWorker> logger, | ||||||
|   SharedTokenService tokenService, |       SharedTokenService tokenService, | ||||||
|   IServiceProvider serviceProvider, |       IServiceProvider serviceProvider, | ||||||
|   IElectoralApiService apiService, |       IElectoralApiService apiService, | ||||||
|   WorkerConfigService configService) |       WorkerConfigService configService) | ||||||
|   { |   { | ||||||
|     _logger = logger; |     _logger = logger; | ||||||
|     _tokenService = tokenService; |     _tokenService = tokenService; | ||||||
| @@ -34,14 +32,20 @@ public class LowPriorityDataWorker : BackgroundService | |||||||
|  |  | ||||||
|   protected override async Task ExecuteAsync(CancellationToken stoppingToken) |   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); |     await SincronizarCatalogosMaestrosAsync(stoppingToken); | ||||||
|  |  | ||||||
|     while (!stoppingToken.IsCancellationRequested) |     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); |       var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken); | ||||||
|       if (string.IsNullOrEmpty(authToken)) |       if (string.IsNullOrEmpty(authToken)) | ||||||
| @@ -51,25 +55,10 @@ public class LowPriorityDataWorker : BackgroundService | |||||||
|         continue; |         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 |       try | ||||||
|       { |       { | ||||||
|         await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); |         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<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|       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<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|             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); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /// <summary> |   /// <summary> | ||||||
|   /// Obtiene y actualiza el resumen de votos y el estado del recuento a nivel provincial para CADA categoría. |   /// Sondea la proyección de bancas a nivel Provincial y por Sección Electoral. | ||||||
|   /// Este método itera sobre todas las provincias y categorías, obteniendo sus resultados consolidados |   /// Esta versión es completamente robusta: maneja respuestas de API vacías o con fechas mal formadas, | ||||||
|   /// y guardándolos en las tablas 'ResumenesVotos' y 'EstadosRecuentosGenerales'. |   /// guarda la CategoriaId y usa una transacción atómica para la escritura en base de datos. | ||||||
|   /// </summary> |  | ||||||
|   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<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|       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."); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /// <summary> |  | ||||||
|   /// 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. |  | ||||||
|   /// </summary> |   /// </summary> | ||||||
|   /// <param name="authToken">El token de autenticación válido para la sesión.</param> |   /// <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> |   /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | ||||||
|   private async Task SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken) |   private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) | ||||||
|   { |   { | ||||||
|     try |     try | ||||||
|     { |     { | ||||||
|       using var scope = _serviceProvider.CreateScope(); |       using var scope = _serviceProvider.CreateScope(); | ||||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|       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() |           .AsNoTracking() | ||||||
|           .Where(a => a.NivelId == 10 && a.DistritoId != null) |           .Where(a => a.NivelId == 10 && a.DistritoId != null) | ||||||
|           .ToListAsync(stoppingToken); |           .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|       // Busca NivelId 1 (País) o 0 como fallback. |       if (!categoriasDeBancas.Any() || !ambitosASondear.Any()) | ||||||
|       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."); |         _logger.LogWarning("No se encontraron categorías de bancas o ámbitos provinciales en la BD. Omitiendo sondeo de bancas."); | ||||||
|         return; |         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 |       var todasLasProyecciones = new List<ProyeccionBanca>(); | ||||||
|       foreach (var provincia in provinciasASondear) |       bool hasReceivedAnyNewData = false; | ||||||
|  |       var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); | ||||||
|  |  | ||||||
|  |       foreach (var ambito in ambitosASondear) | ||||||
|       { |       { | ||||||
|         if (stoppingToken.IsCancellationRequested) break; |         if (stoppingToken.IsCancellationRequested) break; | ||||||
|         foreach (var categoria in categoriasParaSondear) |         foreach (var categoria in categoriasDeBancas) | ||||||
|         { |         { | ||||||
|           if (stoppingToken.IsCancellationRequested) break; |           if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |  | ||||||
|           var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id); |           var repartoBancasDto = await _apiService.GetBancasAsync(authToken, ambito.DistritoId!, ambito.SeccionProvincialId, categoria.Id); | ||||||
|           if (estadoDto != null) |           if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) | ||||||
|           { |           { | ||||||
|             var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { provincia.Id, categoria.Id }, stoppingToken); |             hasReceivedAnyNewData = true; | ||||||
|             if (registroDb == null) |             DateTime fechaTotalizacion; | ||||||
|  |             if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) | ||||||
|             { |             { | ||||||
|               registroDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = provincia.Id, CategoriaId = categoria.Id }; |               fechaTotalizacion = DateTime.Now; | ||||||
|               dbContext.EstadosRecuentosGenerales.Add(registroDb); |             } | ||||||
|  |             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 (hasReceivedAnyNewData) | ||||||
|       if (ambitoNacional != null && !stoppingToken.IsCancellationRequested) |  | ||||||
|       { |       { | ||||||
|         _logger.LogInformation("Sondeando totales a nivel Nacional (Ambito ID: {ambitoId})...", ambitoNacional.Id); |         _logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos..."); | ||||||
|         foreach (var categoria in categoriasParaSondear) |         await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); | ||||||
|  |  | ||||||
|  |         // Guardar nuevas agrupaciones si las hay | ||||||
|  |         if (dbContext.ChangeTracker.HasChanges()) | ||||||
|         { |         { | ||||||
|           if (stoppingToken.IsCancellationRequested) break; |           await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|  |  | ||||||
|           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 |         // --- LÍNEA CORREGIDA --- | ||||||
|       if (dbContext.ChangeTracker.HasChanges()) |         // 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); |         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 |       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) |     catch (OperationCanceledException) | ||||||
|     { |     { | ||||||
|       _logger.LogInformation("Sondeo de Estado Recuento General cancelado."); |       _logger.LogInformation("Sondeo de bancas cancelado."); | ||||||
|     } |     } | ||||||
|     catch (Exception ex) |     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."); |       _logger.LogError(ex, "Ocurrió un error CRÍTICO durante la sincronización de catálogos."); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /// <summary> |  | ||||||
|   /// 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. |  | ||||||
|   /// </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 SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) |  | ||||||
|   { |  | ||||||
|     try |  | ||||||
|     { |  | ||||||
|       using var scope = _serviceProvider.CreateScope(); |  | ||||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|       var categoriasDeBancas = await dbContext.CategoriasElectorales |  | ||||||
|           .AsNoTracking() |  | ||||||
|           .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) |  | ||||||
|           .ToListAsync(stoppingToken); |  | ||||||
|  |  | ||||||
|       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<ProyeccionBanca>(); |  | ||||||
|       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."); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /// <summary> |  | ||||||
|   /// 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. |  | ||||||
|   /// </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 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<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|       // 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<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|             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."); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user