feat(alta): dataset inicial al crear familia y niño

Al registrar una familia se siembra el catálogo de materiales escolares
típicos y el catálogo de rutinas. Al dar de alta un niño se le asignan
las rutinas de tarde de L-V escaladas por su edad (tramos 8-9 / 10-11 /
12-14), reutilizando el catálogo de la familia.
This commit is contained in:
Jaume Garriga Maestre
2026-06-22 23:14:35 +02:00
parent b0069a0c9e
commit 1f968e82cd
4 changed files with 204 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ package es.asepeyo.recordalexia.security;
import es.asepeyo.recordalexia.domain.Family;
import es.asepeyo.recordalexia.exception.ConflictException;
import es.asepeyo.recordalexia.repository.FamilyRepository;
import es.asepeyo.recordalexia.service.InitialDatasetService;
import java.util.Optional;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@@ -19,12 +20,14 @@ public class AuthService {
private final FamilyRepository familyRepository;
private final SessionAuthService sessions;
private final PasswordEncoder encoder;
private final InitialDatasetService initialDataset;
public AuthService(FamilyRepository familyRepository, SessionAuthService sessions,
PasswordEncoder encoder) {
PasswordEncoder encoder, InitialDatasetService initialDataset) {
this.familyRepository = familyRepository;
this.sessions = sessions;
this.encoder = encoder;
this.initialDataset = initialDataset;
}
/** Registra una familia nueva y abre sesión (auto-login). Devuelve el handle. */
@@ -36,6 +39,8 @@ public class AuthService {
}
Family family = familyRepository.save(
new Family(normalized, encoder.encode(rawPass), name, encoder.encode(rawPin)));
// Sembrar el catálogo inicial (materiales + rutinas) para no empezar de cero.
initialDataset.provisionFamily(family);
return sessions.openSession(family);
}

View File

@@ -25,12 +25,14 @@ public class ChildService {
private final ChildRepository childRepository;
private final FamilyRepository familyRepository;
private final FamilyContext familyContext;
private final InitialDatasetService initialDataset;
public ChildService(ChildRepository childRepository, FamilyRepository familyRepository,
FamilyContext familyContext) {
FamilyContext familyContext, InitialDatasetService initialDataset) {
this.childRepository = childRepository;
this.familyRepository = familyRepository;
this.familyContext = familyContext;
this.initialDataset = initialDataset;
}
@Transactional(readOnly = true)
@@ -89,7 +91,10 @@ public class ChildService {
// El niño nace en la familia de la sesión.
child.setFamily(familyRepository.getReferenceById(familyContext.currentFamilyId()));
applyRequest(child, req);
return childRepository.save(child);
Child saved = childRepository.save(child);
// Sembrar las rutinas de tarde acordes a su edad (van tomando responsabilidades).
initialDataset.provisionChildRoutines(saved);
return saved;
}
@Transactional

View File

@@ -0,0 +1,125 @@
package es.asepeyo.recordalexia.service;
import es.asepeyo.recordalexia.domain.AfternoonRoutine;
import es.asepeyo.recordalexia.domain.Child;
import es.asepeyo.recordalexia.domain.Family;
import es.asepeyo.recordalexia.domain.MaterialItem;
import es.asepeyo.recordalexia.domain.RoutineTask;
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
import es.asepeyo.recordalexia.repository.MaterialItemRepository;
import es.asepeyo.recordalexia.repository.RoutineTaskRepository;
import java.time.DayOfWeek;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Provisión inicial de datos al dar de alta una familia o un niño, para que no haya
* que crearlo todo a mano:
* - Familia nueva: catálogo de materiales escolares típicos + catálogo de rutinas.
* - Niño nuevo: rutinas de tarde de lunes a viernes, escalonadas por su edad (van
* tomando responsabilidades según el tramo 8-9 / 10-11 / 12-14), referenciando las
* rutinas del catálogo de su familia.
*/
@Service
public class InitialDatasetService {
private static final List<DayOfWeek> LABORABLES = List.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY,
DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY);
/** Material escolar típico que se siembra en toda familia nueva. */
private static final List<MaterialSeed> MATERIALES = List.of(
new MaterialSeed("Estuche", "Estoig", "✏️", "#F4C95D", "general"),
new MaterialSeed("Agenda", "Agenda", "📔", "#5B8DEF", "general"),
new MaterialSeed("Libreta", "Llibreta", "📓", "#A78BD0", "general"),
new MaterialSeed("Libro de texto", "Llibre de text", "📘", "#5B8DEF", "general"),
new MaterialSeed("Lectura", "Lectura", "📖", "#F2A65A", "lengua"),
new MaterialSeed("Almuerzo", "Esmorzar", "🍎", "#EC8FA4", "general"),
new MaterialSeed("Botella de agua", "Ampolla d'aigua", "💧", "#5BC0BE", "general"),
new MaterialSeed("Ropa de gimnasia", "Roba d'EF", "👕", "#7FBF6B", "gimnasia"),
new MaterialSeed("Zapatillas", "Sabatilles", "👟", "#7FBF6B", "gimnasia"));
/**
* Catálogo de rutinas con la edad mínima a partir de la cual se asigna al niño.
* minAge 0 = base (todos), 10 = a partir de 10-11, 12 = a partir de 12-14.
*/
private static final List<RoutineSeed> RUTINAS = List.of(
// Base: autonomía básica (8-9 y también válidas para más pequeños).
new RoutineSeed("Deshacer la mochila", "Buidar la motxilla", "🎒", "#F2A65A", 0),
new RoutineSeed("Merendar", "Berenar", "🥪", "#F4C95D", 0),
new RoutineSeed("Hacer los deberes", "Fer els deures", "📝", "#5B8DEF", 0),
new RoutineSeed("Preparar la mochila", "Preparar la motxilla", "🎒", "#A78BD0", 0),
new RoutineSeed("Lavarse los dientes", "Rentar-se les dents", "🪥", "#5BC0BE", 0),
// 10-11: organización y primeras tareas de casa.
new RoutineSeed("Poner y recoger la mesa", "Parar i recollir taula", "🍽️", "#7FBF6B", 10),
new RoutineSeed("Hacer la cama", "Fer el llit", "🛏️", "#EC8FA4", 10),
new RoutineSeed("Revisar la agenda", "Revisar l'agenda", "📔", "#5B8DEF", 10),
new RoutineSeed("Leer 20 minutos", "Llegir 20 minuts", "📖", "#F2A65A", 10),
// 12-14: corresponsabilidad y gestión propia.
new RoutineSeed("Planificar la semana", "Planificar la setmana", "🗓️", "#A78BD0", 12),
new RoutineSeed("Ayudar a preparar la cena", "Ajudar a fer el sopar", "🍳", "#F2A65A", 12),
new RoutineSeed("Ocuparme de mi ropa", "Ocupar-me de la meva roba", "🧺", "#5BC0BE", 12),
new RoutineSeed("Estudio autónomo", "Estudi autònom", "📚", "#5B8DEF", 12),
new RoutineSeed("Tarea de casa de la semana", "Tasca de casa de la setmana", "🧹", "#7FBF6B", 12));
private final MaterialItemRepository materialRepository;
private final RoutineTaskRepository routineTaskRepository;
private final AfternoonRoutineRepository routineRepository;
public InitialDatasetService(MaterialItemRepository materialRepository,
RoutineTaskRepository routineTaskRepository,
AfternoonRoutineRepository routineRepository) {
this.materialRepository = materialRepository;
this.routineTaskRepository = routineTaskRepository;
this.routineRepository = routineRepository;
}
/** Siembra el catálogo (materiales + rutinas) de una familia recién creada. */
@Transactional
public void provisionFamily(Family family) {
for (MaterialSeed m : MATERIALES) {
MaterialItem item = new MaterialItem(m.es(), m.ca(), m.icon(), m.color(), m.category());
item.setFamily(family);
materialRepository.save(item);
}
for (RoutineSeed r : RUTINAS) {
RoutineTask task = new RoutineTask(r.es(), r.ca(), r.icon(), r.color());
task.setFamily(family);
routineTaskRepository.save(task);
}
}
/**
* Asigna a un niño recién creado las rutinas de tarde acordes a su edad, de lunes a
* viernes. Reutiliza las rutinas del catálogo de su familia (las que sembró
* {@link #provisionFamily}); si la familia no tiene catálogo, no hace nada.
*/
@Transactional
public void provisionChildRoutines(Child child) {
List<RoutineTask> catalogo = routineTaskRepository.findByFamilyId(child.getFamily().getId());
// Rutinas que corresponden a la edad del niño, en el orden definido en RUTINAS.
List<RoutineTask> aplicables = RUTINAS.stream()
.filter(r -> child.getAge() >= r.minAge())
.map(r -> buscarEnCatalogo(catalogo, r.es()))
.filter(java.util.Objects::nonNull)
.toList();
for (DayOfWeek dia : LABORABLES) {
int orden = 0;
for (RoutineTask tarea : aplicables) {
routineRepository.save(new AfternoonRoutine(child, dia, tarea, orden++));
}
}
}
/** Localiza en el catálogo la rutina por su etiqueta ES (clave estable del seed). */
private RoutineTask buscarEnCatalogo(List<RoutineTask> catalogo, String labelEs) {
return catalogo.stream().filter(t -> t.getLabelEs().equals(labelEs)).findFirst().orElse(null);
}
private record MaterialSeed(String es, String ca, String icon, String color, String category) {
}
private record RoutineSeed(String es, String ca, String icon, String color, int minAge) {
}
}

View File

@@ -0,0 +1,66 @@
package es.asepeyo.recordalexia.service;
import static org.assertj.core.api.Assertions.assertThat;
import es.asepeyo.recordalexia.domain.Child;
import es.asepeyo.recordalexia.domain.Family;
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
import es.asepeyo.recordalexia.repository.ChildRepository;
import es.asepeyo.recordalexia.repository.FamilyRepository;
import es.asepeyo.recordalexia.repository.MaterialItemRepository;
import es.asepeyo.recordalexia.repository.RoutineTaskRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
/** Provisión inicial: catálogo al alta de familia y rutinas escalonadas al alta de niño. */
@SpringBootTest
@Transactional
class InitialDatasetServiceTest {
@Autowired private InitialDatasetService initialDataset;
@Autowired private FamilyRepository familyRepository;
@Autowired private ChildRepository childRepository;
@Autowired private MaterialItemRepository materialRepository;
@Autowired private RoutineTaskRepository routineTaskRepository;
@Autowired private AfternoonRoutineRepository routineRepository;
@Test
void elAltaDeFamiliaSiembraElCatalogo() {
Family family = familyRepository.save(new Family("cat@x.com", "h", "Cat", "p"));
initialDataset.provisionFamily(family);
assertThat(materialRepository.findByFamilyId(family.getId())).hasSize(9);
assertThat(routineTaskRepository.findByFamilyId(family.getId())).hasSize(14);
}
@Test
void lasRutinasDelNinoSeEscalanPorEdad() {
Family family = familyRepository.save(new Family("edad@x.com", "h", "Edad", "p"));
initialDataset.provisionFamily(family);
// 8 años → solo el set base (5 rutinas) x 5 días laborables = 25.
Child pequeno = childRepository.save(nino(family, 8));
initialDataset.provisionChildRoutines(pequeno);
assertThat(routineRepository.findByChildIdOrderByDayOfWeekAscOrderIndexAsc(pequeno.getId()))
.hasSize(5 * 5);
// 13 años → todas las rutinas (14) x 5 días = 70.
Child mayor = childRepository.save(nino(family, 13));
initialDataset.provisionChildRoutines(mayor);
assertThat(routineRepository.findByChildIdOrderByDayOfWeekAscOrderIndexAsc(mayor.getId()))
.hasSize(14 * 5);
}
private Child nino(Family family, int edad) {
Child c = new Child();
c.setFamily(family);
c.setName("Test");
c.setMascot("🦊");
c.setAccentColor("#F2A65A");
c.setAge(edad);
return c;
}
}