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:
@@ -3,6 +3,7 @@ package es.asepeyo.recordalexia.security;
|
|||||||
import es.asepeyo.recordalexia.domain.Family;
|
import es.asepeyo.recordalexia.domain.Family;
|
||||||
import es.asepeyo.recordalexia.exception.ConflictException;
|
import es.asepeyo.recordalexia.exception.ConflictException;
|
||||||
import es.asepeyo.recordalexia.repository.FamilyRepository;
|
import es.asepeyo.recordalexia.repository.FamilyRepository;
|
||||||
|
import es.asepeyo.recordalexia.service.InitialDatasetService;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -19,12 +20,14 @@ public class AuthService {
|
|||||||
private final FamilyRepository familyRepository;
|
private final FamilyRepository familyRepository;
|
||||||
private final SessionAuthService sessions;
|
private final SessionAuthService sessions;
|
||||||
private final PasswordEncoder encoder;
|
private final PasswordEncoder encoder;
|
||||||
|
private final InitialDatasetService initialDataset;
|
||||||
|
|
||||||
public AuthService(FamilyRepository familyRepository, SessionAuthService sessions,
|
public AuthService(FamilyRepository familyRepository, SessionAuthService sessions,
|
||||||
PasswordEncoder encoder) {
|
PasswordEncoder encoder, InitialDatasetService initialDataset) {
|
||||||
this.familyRepository = familyRepository;
|
this.familyRepository = familyRepository;
|
||||||
this.sessions = sessions;
|
this.sessions = sessions;
|
||||||
this.encoder = encoder;
|
this.encoder = encoder;
|
||||||
|
this.initialDataset = initialDataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Registra una familia nueva y abre sesión (auto-login). Devuelve el handle. */
|
/** Registra una familia nueva y abre sesión (auto-login). Devuelve el handle. */
|
||||||
@@ -36,6 +39,8 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
Family family = familyRepository.save(
|
Family family = familyRepository.save(
|
||||||
new Family(normalized, encoder.encode(rawPass), name, encoder.encode(rawPin)));
|
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);
|
return sessions.openSession(family);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,14 @@ public class ChildService {
|
|||||||
private final ChildRepository childRepository;
|
private final ChildRepository childRepository;
|
||||||
private final FamilyRepository familyRepository;
|
private final FamilyRepository familyRepository;
|
||||||
private final FamilyContext familyContext;
|
private final FamilyContext familyContext;
|
||||||
|
private final InitialDatasetService initialDataset;
|
||||||
|
|
||||||
public ChildService(ChildRepository childRepository, FamilyRepository familyRepository,
|
public ChildService(ChildRepository childRepository, FamilyRepository familyRepository,
|
||||||
FamilyContext familyContext) {
|
FamilyContext familyContext, InitialDatasetService initialDataset) {
|
||||||
this.childRepository = childRepository;
|
this.childRepository = childRepository;
|
||||||
this.familyRepository = familyRepository;
|
this.familyRepository = familyRepository;
|
||||||
this.familyContext = familyContext;
|
this.familyContext = familyContext;
|
||||||
|
this.initialDataset = initialDataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@@ -89,7 +91,10 @@ public class ChildService {
|
|||||||
// El niño nace en la familia de la sesión.
|
// El niño nace en la familia de la sesión.
|
||||||
child.setFamily(familyRepository.getReferenceById(familyContext.currentFamilyId()));
|
child.setFamily(familyRepository.getReferenceById(familyContext.currentFamilyId()));
|
||||||
applyRequest(child, req);
|
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
|
@Transactional
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user