diff --git a/lib/servicios/servicio_alarmas.dart b/lib/servicios/servicio_alarmas.dart index adadf0b..97c7cc9 100644 --- a/lib/servicios/servicio_alarmas.dart +++ b/lib/servicios/servicio_alarmas.dart @@ -350,6 +350,62 @@ class ServicioAlarmas { return nuevo; }); + /// Records a scheduling-reliability failure for [alarmaId] (fix/alarmas- + /// fallos-silenciosos): reuses the SAME `ExcepcionAlarma` model + /// `saltarProxima` already persists, so `EstadoAlarmas.ultimaExcepcionPara` + /// surfaces it on the exact alarm card affected instead of only a + /// transient, alarm-agnostic message. A previous FAILURE record for the + /// same alarm is replaced (only the latest attempt's outcome matters) -- + /// any `skipNext` exception for this or other alarms is left untouched. + /// Never affects scheduling: `ServicioProgramacionAlarmas._esValida` only + /// treats `tipoSaltoSiguiente` as an actual skip. + Future registrarFalloProgramacion( + String alarmaId, + DateTime ejecucion, + String tipo, + ) => _enCola(() async { + final config = await _configActual(); + final excepciones = [ + ..._sinFalloPrevio(config.excepciones, alarmaId), + ExcepcionAlarma(alarmaId: alarmaId, ejecucion: ejecucion, tipo: tipo), + ]; + final nuevo = ConfiguracionAlarmas( + alarmas: config.alarmas, + vacaciones: config.vacaciones, + excepciones: excepciones, + ); + await _guardar(nuevo); + return nuevo; + }); + + /// Clears any outstanding failure record for [alarmaId] (a subsequent + /// scheduling attempt succeeded). No-op when there is nothing to clear. + Future limpiarFalloProgramacion( + String alarmaId, + ) => _enCola(() async { + final config = await _configActual(); + final sinFallo = _sinFalloPrevio(config.excepciones, alarmaId); + if (sinFallo.length == config.excepciones.length) return config; + final nuevo = ConfiguracionAlarmas( + alarmas: config.alarmas, + vacaciones: config.vacaciones, + excepciones: sinFallo, + ); + await _guardar(nuevo); + return nuevo; + }); + + List _sinFalloPrevio( + List excepciones, + String alarmaId, + ) => excepciones + .where( + (e) => + !(e.alarmaId == alarmaId && + ExcepcionAlarma.tiposFallo.contains(e.tipo)), + ) + .toList(); + Future posponerEjecucion( String alarmaId, DateTime ejecucion, diff --git a/test/servicios/servicio_alarmas_fallo_programacion_test.dart b/test/servicios/servicio_alarmas_fallo_programacion_test.dart new file mode 100644 index 0000000..72b0370 --- /dev/null +++ b/test/servicios/servicio_alarmas_fallo_programacion_test.dart @@ -0,0 +1,150 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:pluriwave/modelos/alarma_musical.dart'; +import 'package:pluriwave/servicios/servicio_alarmas.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Coverage for the scheduling-reliability failure records (fix/alarmas- +/// fallos-silenciosos): a failed `android.programar` call is no longer only +/// a transient, alarm-agnostic `EstadoAlarmas.error` string -- it is also +/// recorded per-alarm through the SAME `ExcepcionAlarma` model `saltarProxima` +/// already uses, so `EstadoAlarmas.ultimaExcepcionPara` can surface it on the +/// exact card affected. +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('registrarFalloProgramacion agrega una excepcion de fallo para la ' + 'alarma indicada', () async { + final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)); + await servicio.guardarAlarma( + AlarmaMusical( + id: 'a1', + nombre: 'Diaria', + hora: 7, + minuto: 30, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: const [], + ), + ); + + final config = await servicio.registrarFalloProgramacion( + 'a1', + DateTime(2026, 5, 25, 7, 30), + ExcepcionAlarma.tipoFalloProgramacion, + ); + + expect(config.excepciones, hasLength(1)); + expect(config.excepciones.single.alarmaId, 'a1'); + expect( + config.excepciones.single.tipo, + ExcepcionAlarma.tipoFalloProgramacion, + ); + }); + + test('registrarFalloProgramacion reemplaza un fallo previo de la MISMA ' + 'alarma en vez de acumular', () async { + final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)); + await servicio.guardarAlarma( + AlarmaMusical( + id: 'a1', + nombre: 'Diaria', + hora: 7, + minuto: 30, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: const [], + ), + ); + + await servicio.registrarFalloProgramacion( + 'a1', + DateTime(2026, 5, 25, 7, 30), + ExcepcionAlarma.tipoFalloPreaviso, + ); + final config = await servicio.registrarFalloProgramacion( + 'a1', + DateTime(2026, 5, 26, 7, 30), + ExcepcionAlarma.tipoFalloProgramacion, + ); + + expect(config.excepciones, hasLength(1)); + expect( + config.excepciones.single.tipo, + ExcepcionAlarma.tipoFalloProgramacion, + ); + }); + + test('registrarFalloProgramacion NUNCA toca las excepciones skipNext de ' + 'otras alarmas ni de la misma', () async { + final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)); + await servicio.guardarAlarma( + AlarmaMusical( + id: 'a1', + nombre: 'Diaria', + hora: 7, + minuto: 30, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: const [], + ), + ); + await servicio.saltarProxima('a1'); + + final config = await servicio.registrarFalloProgramacion( + 'a1', + DateTime(2026, 5, 26, 7, 30), + ExcepcionAlarma.tipoFalloProgramacion, + ); + + expect(config.excepciones, hasLength(2)); + expect( + config.excepciones.map((e) => e.tipo), + containsAll([ + ExcepcionAlarma.tipoSaltoSiguiente, + ExcepcionAlarma.tipoFalloProgramacion, + ]), + ); + }); + + test('limpiarFalloProgramacion elimina el fallo registrado para esa ' + 'alarma', () async { + final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)); + await servicio.guardarAlarma( + AlarmaMusical( + id: 'a1', + nombre: 'Diaria', + hora: 7, + minuto: 30, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: const [], + ), + ); + await servicio.registrarFalloProgramacion( + 'a1', + DateTime(2026, 5, 25, 7, 30), + ExcepcionAlarma.tipoFalloProgramacion, + ); + + final config = await servicio.limpiarFalloProgramacion('a1'); + + expect(config.excepciones, isEmpty); + }); + + test('limpiarFalloProgramacion sin fallo previo no rompe y no persiste ' + 'cambios', () async { + final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)); + await servicio.guardarAlarma( + AlarmaMusical( + id: 'a1', + nombre: 'Diaria', + hora: 7, + minuto: 30, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: const [], + ), + ); + + final config = await servicio.limpiarFalloProgramacion('a1'); + + expect(config.excepciones, isEmpty); + }); +}