feat(rutinas): catálogo de rutinas reutilizable por familia

Normaliza las rutinas de tarde replicando el patrón de las mañanas:
RoutineTask es el catálogo (gemela de Activity) y AfternoonRoutine
pasa a ser pura asignación niño+día. Incluye migración Liquibase 003
con consolidación de duplicados y rollback, y la cascada de monedas
(override del día -> catálogo -> coinsPerTask) en DayGenerationService.
This commit is contained in:
Jaume Garriga Maestre
2026-06-22 22:41:19 +02:00
parent fd184f769f
commit a58d7bda78
8 changed files with 360 additions and 61 deletions

View File

@@ -7,6 +7,7 @@ import es.asepeyo.recordalexia.domain.EventType;
import es.asepeyo.recordalexia.domain.Family; import es.asepeyo.recordalexia.domain.Family;
import es.asepeyo.recordalexia.domain.MaterialItem; import es.asepeyo.recordalexia.domain.MaterialItem;
import es.asepeyo.recordalexia.domain.Reward; import es.asepeyo.recordalexia.domain.Reward;
import es.asepeyo.recordalexia.domain.RoutineTask;
import es.asepeyo.recordalexia.domain.SpecialEvent; import es.asepeyo.recordalexia.domain.SpecialEvent;
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry; import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
import es.asepeyo.recordalexia.repository.ActivityRepository; import es.asepeyo.recordalexia.repository.ActivityRepository;
@@ -15,6 +16,7 @@ import es.asepeyo.recordalexia.repository.ChildRepository;
import es.asepeyo.recordalexia.repository.FamilyRepository; import es.asepeyo.recordalexia.repository.FamilyRepository;
import es.asepeyo.recordalexia.repository.MaterialItemRepository; import es.asepeyo.recordalexia.repository.MaterialItemRepository;
import es.asepeyo.recordalexia.repository.RewardRepository; import es.asepeyo.recordalexia.repository.RewardRepository;
import es.asepeyo.recordalexia.repository.RoutineTaskRepository;
import es.asepeyo.recordalexia.repository.SpecialEventRepository; import es.asepeyo.recordalexia.repository.SpecialEventRepository;
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository; import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
import java.time.Clock; import java.time.Clock;
@@ -51,6 +53,7 @@ public class DataSeeder implements ApplicationRunner {
private final ActivityRepository activityRepository; private final ActivityRepository activityRepository;
private final WeeklyTemplateEntryRepository templateRepository; private final WeeklyTemplateEntryRepository templateRepository;
private final AfternoonRoutineRepository routineRepository; private final AfternoonRoutineRepository routineRepository;
private final RoutineTaskRepository routineTaskRepository;
private final SpecialEventRepository eventRepository; private final SpecialEventRepository eventRepository;
private final RewardRepository rewardRepository; private final RewardRepository rewardRepository;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
@@ -60,6 +63,7 @@ public class DataSeeder implements ApplicationRunner {
MaterialItemRepository materialRepository, ActivityRepository activityRepository, MaterialItemRepository materialRepository, ActivityRepository activityRepository,
WeeklyTemplateEntryRepository templateRepository, WeeklyTemplateEntryRepository templateRepository,
AfternoonRoutineRepository routineRepository, AfternoonRoutineRepository routineRepository,
RoutineTaskRepository routineTaskRepository,
SpecialEventRepository eventRepository, RewardRepository rewardRepository, SpecialEventRepository eventRepository, RewardRepository rewardRepository,
PasswordEncoder passwordEncoder, Clock clock) { PasswordEncoder passwordEncoder, Clock clock) {
this.familyRepository = familyRepository; this.familyRepository = familyRepository;
@@ -68,6 +72,7 @@ public class DataSeeder implements ApplicationRunner {
this.activityRepository = activityRepository; this.activityRepository = activityRepository;
this.templateRepository = templateRepository; this.templateRepository = templateRepository;
this.routineRepository = routineRepository; this.routineRepository = routineRepository;
this.routineTaskRepository = routineTaskRepository;
this.eventRepository = eventRepository; this.eventRepository = eventRepository;
this.rewardRepository = rewardRepository; this.rewardRepository = rewardRepository;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
@@ -86,6 +91,7 @@ public class DataSeeder implements ApplicationRunner {
var materials = seedMaterials(family); var materials = seedMaterials(family);
var activities = seedActivities(family, materials); var activities = seedActivities(family, materials);
var routines = seedRoutineCatalog(family);
seedRewards(family); seedRewards(family);
Child nora = childRepository.save(child(family, "Nora", "🦊", "#F2A65A", 7, 42, LocalTime.of(8, 30))); Child nora = childRepository.save(child(family, "Nora", "🦊", "#F2A65A", 7, 42, LocalTime.of(8, 30)));
@@ -94,7 +100,7 @@ public class DataSeeder implements ApplicationRunner {
for (Child c : List.of(nora, leo, mia)) { for (Child c : List.of(nora, leo, mia)) {
seedWeeklyMornings(c, activities); seedWeeklyMornings(c, activities);
seedAfternoonRoutines(c); seedAfternoonRoutines(c, routines);
} }
LocalDate today = LocalDate.now(clock); LocalDate today = LocalDate.now(clock);
@@ -185,19 +191,31 @@ public class DataSeeder implements ApplicationRunner {
templateRepository.save(new WeeklyTemplateEntry(child, day, activity, 0)); templateRepository.save(new WeeklyTemplateEntry(child, day, activity, 0));
} }
private void seedAfternoonRoutines(Child child) { /** Catálogo de rutinas reutilizables de la familia (se definen una sola vez). */
private RoutinesCatalog seedRoutineCatalog(Family family) {
return new RoutinesCatalog(
routineTask(family, "Deshacer la mochila", "Buidar la motxilla", "🎒", "#F2A65A"),
routineTask(family, "Merendar", "Berenar", "🥪", "#F4C95D"),
routineTask(family, "Hacer los deberes", "Fer els deures", "📝", "#5B8DEF"),
routineTask(family, "Practicar piano", "Practicar piano", "🎹", "#A78BD0"),
routineTask(family, "Recoger la mesa", "Parar taula", "🍽️", "#7FBF6B"));
}
private RoutineTask routineTask(Family family, String es, String ca, String icon, String color) {
RoutineTask rt = new RoutineTask(es, ca, icon, color);
rt.setFamily(family);
return routineTaskRepository.save(rt);
}
/** Asigna las rutinas del catálogo a cada día laborable, reutilizando las mismas. */
private void seedAfternoonRoutines(Child child, RoutinesCatalog r) {
for (DayOfWeek day : List.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, for (DayOfWeek day : List.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY,
DayOfWeek.THURSDAY, DayOfWeek.FRIDAY)) { DayOfWeek.THURSDAY, DayOfWeek.FRIDAY)) {
routineRepository.save(new AfternoonRoutine(child, day, "Deshacer la mochila", routineRepository.save(new AfternoonRoutine(child, day, r.mochila, 0));
"Buidar la motxilla", "🎒", "#F2A65A", 0)); routineRepository.save(new AfternoonRoutine(child, day, r.merienda, 1));
routineRepository.save(new AfternoonRoutine(child, day, "Merendar", routineRepository.save(new AfternoonRoutine(child, day, r.deberes, 2));
"Berenar", "🥪", "#F4C95D", 1)); routineRepository.save(new AfternoonRoutine(child, day, r.piano, 3));
routineRepository.save(new AfternoonRoutine(child, day, "Hacer los deberes", routineRepository.save(new AfternoonRoutine(child, day, r.mesa, 4));
"Fer els deures", "📝", "#5B8DEF", 2));
routineRepository.save(new AfternoonRoutine(child, day, "Practicar piano",
"Practicar piano", "🎹", "#A78BD0", 3));
routineRepository.save(new AfternoonRoutine(child, day, "Recoger la mesa",
"Parar taula", "🍽️", "#7FBF6B", 4));
} }
} }
@@ -210,4 +228,8 @@ public class DataSeeder implements ApplicationRunner {
private record ActivitiesCatalog(Activity gimnasia, Activity musica, Activity matematicas, private record ActivitiesCatalog(Activity gimnasia, Activity musica, Activity matematicas,
Activity lengua) { Activity lengua) {
} }
private record RoutinesCatalog(RoutineTask mochila, RoutineTask merienda, RoutineTask deberes,
RoutineTask piano, RoutineTask mesa) {
}
} }

View File

@@ -13,7 +13,12 @@ import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.DayOfWeek; import java.time.DayOfWeek;
/** Rutina de tarde recurrente de un niño para un día de la semana (reordenable). */ /**
* Asignación de una rutina del catálogo ({@link RoutineTask}) a un niño para un día de
* la semana (reordenable). Es la gemela de tarde de {@link WeeklyTemplateEntry}: aquí
* NO vive la definición (texto, emoji, color), que es reutilizable y se lee del
* {@link RoutineTask} referenciado. Así una misma rutina sirve para varios días y niños.
*/
@Entity @Entity
@Table(name = "afternoon_routine") @Table(name = "afternoon_routine")
public class AfternoonRoutine { public class AfternoonRoutine {
@@ -30,33 +35,24 @@ public class AfternoonRoutine {
@Column(name = "day_of_week") @Column(name = "day_of_week")
private DayOfWeek dayOfWeek; private DayOfWeek dayOfWeek;
@Column(name = "label_es") @ManyToOne(fetch = FetchType.LAZY, optional = false)
private String labelEs; @JoinColumn(name = "routine_task_id")
private RoutineTask routineTask;
@Column(name = "label_ca")
private String labelCa;
private String icon;
private String color;
@Column(name = "order_index") @Column(name = "order_index")
private int orderIndex; private int orderIndex;
/** Monedas de la rutina; si es null se usa coinsPerTask del niño. */ /** Monedas para este día concreto; si es null se usa la cascada catálogo→niño. */
@Column(name = "coins_reward") @Column(name = "coins_reward")
private Integer coinsReward; private Integer coinsReward;
protected AfternoonRoutine() { protected AfternoonRoutine() {
} }
public AfternoonRoutine(Child child, DayOfWeek dayOfWeek, String labelEs, String labelCa, public AfternoonRoutine(Child child, DayOfWeek dayOfWeek, RoutineTask routineTask, int orderIndex) {
String icon, String color, int orderIndex) {
this.child = child; this.child = child;
this.dayOfWeek = dayOfWeek; this.dayOfWeek = dayOfWeek;
this.labelEs = labelEs; this.routineTask = routineTask;
this.labelCa = labelCa;
this.icon = icon;
this.color = color;
this.orderIndex = orderIndex; this.orderIndex = orderIndex;
} }
@@ -80,36 +76,12 @@ public class AfternoonRoutine {
this.dayOfWeek = dayOfWeek; this.dayOfWeek = dayOfWeek;
} }
public String getLabelEs() { public RoutineTask getRoutineTask() {
return labelEs; return routineTask;
} }
public void setLabelEs(String labelEs) { public void setRoutineTask(RoutineTask routineTask) {
this.labelEs = labelEs; this.routineTask = routineTask;
}
public String getLabelCa() {
return labelCa;
}
public void setLabelCa(String labelCa) {
this.labelCa = labelCa;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
} }
public int getOrderIndex() { public int getOrderIndex() {

View File

@@ -0,0 +1,105 @@
package es.asepeyo.recordalexia.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
/**
* Rutina reutilizable del catálogo de la familia (ej. "Merendar", "Hacer los deberes").
* Es la DEFINICIÓN: texto bilingüe + emoji + color + monedas por defecto. La asignación
* a un niño y a un día concreto se modela aparte en {@link AfternoonRoutine}, igual que
* {@link Activity} (catálogo de mañana) se asigna mediante {@link WeeklyTemplateEntry}.
*/
@Entity
@Table(name = "routine_task")
public class RoutineTask {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "family_id")
private Family family;
@Column(name = "label_es")
private String labelEs;
@Column(name = "label_ca")
private String labelCa;
private String icon;
private String color;
/** Monedas por defecto de la rutina; si es null, la asignación o el niño deciden. */
@Column(name = "coins_reward")
private Integer coinsReward;
protected RoutineTask() {
}
public RoutineTask(String labelEs, String labelCa, String icon, String color) {
this.labelEs = labelEs;
this.labelCa = labelCa;
this.icon = icon;
this.color = color;
}
public Long getId() {
return id;
}
public Family getFamily() {
return family;
}
public void setFamily(Family family) {
this.family = family;
}
public String getLabelEs() {
return labelEs;
}
public void setLabelEs(String labelEs) {
this.labelEs = labelEs;
}
public String getLabelCa() {
return labelCa;
}
public void setLabelCa(String labelCa) {
this.labelCa = labelCa;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Integer getCoinsReward() {
return coinsReward;
}
public void setCoinsReward(Integer coinsReward) {
this.coinsReward = coinsReward;
}
}

View File

@@ -0,0 +1,14 @@
package es.asepeyo.recordalexia.repository;
import es.asepeyo.recordalexia.domain.RoutineTask;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
/** Catálogo de rutinas de tarde, SIEMPRE scopeado a la familia (tenant). */
public interface RoutineTaskRepository extends JpaRepository<RoutineTask, Long> {
List<RoutineTask> findByFamilyId(Long familyId);
Optional<RoutineTask> findByIdAndFamilyId(Long id, Long familyId);
}

View File

@@ -73,16 +73,36 @@ public class DayGenerationService {
monedas, TaskOrigin.TEMPLATE, entrada.getOrderIndex())); monedas, TaskOrigin.TEMPLATE, entrada.getOrderIndex()));
} }
// --- Tarde: una tarea por cada rutina del día --- // --- Tarde: una tarea por cada rutina asignada al día ---
// La definición (texto, emoji, color) se lee del catálogo (RoutineTask), igual
// que la mañana la lee de Activity. Monedas con cascada: override del día →
// valor por defecto del catálogo → coinsPerTask del niño.
List<AfternoonRoutine> rutinas = List<AfternoonRoutine> rutinas =
routineRepository.findByChildIdAndDayOfWeekOrderByOrderIndexAsc(childId, dayOfWeek); routineRepository.findByChildIdAndDayOfWeekOrderByOrderIndexAsc(childId, dayOfWeek);
for (AfternoonRoutine rutina : rutinas) { for (AfternoonRoutine rutina : rutinas) {
int monedas = rutina.getCoinsReward() != null ? rutina.getCoinsReward() : child.getCoinsPerTask(); var tarea = rutina.getRoutineTask();
int monedas = resolverMonedas(rutina.getCoinsReward(), tarea.getCoinsReward(),
child.getCoinsPerTask());
nuevas.add(new DailyTask(child, date, Slot.AFTERNOON, nuevas.add(new DailyTask(child, date, Slot.AFTERNOON,
rutina.getLabelEs(), rutina.getLabelCa(), rutina.getIcon(), rutina.getColor(), tarea.getLabelEs(), tarea.getLabelCa(), tarea.getIcon(), tarea.getColor(),
monedas, TaskOrigin.ROUTINE, rutina.getOrderIndex())); monedas, TaskOrigin.ROUTINE, rutina.getOrderIndex()));
} }
return dailyTaskRepository.saveAll(nuevas); return dailyTaskRepository.saveAll(nuevas);
} }
/**
* Resuelve las monedas de una rutina de tarde por precedencia: primero el override
* del día (asignación), si no el valor por defecto del catálogo, y como último
* recurso las monedas por tarea del niño.
*/
private int resolverMonedas(Integer overrideDia, Integer porDefectoCatalogo, int porTareaNino) {
if (overrideDia != null) {
return overrideDia;
}
if (porDefectoCatalogo != null) {
return porDefectoCatalogo;
}
return porTareaNino;
}
} }

View File

@@ -0,0 +1,117 @@
# Catálogo reutilizable de rutinas de tarde.
# Normaliza afternoon_routine: la definición (texto/emoji/color) pasa a routine_task
# (catálogo por familia) y afternoon_routine queda como pura asignación niño+día.
# Migra los datos existentes deduplicando las rutinas repetidas por día y familia.
# Cada changeset lleva rollback explícito (Liquibase los revierte en orden inverso).
databaseChangeLog:
- changeSet:
id: 300-create-routine-task
author: recordalexia
changes:
- createTable:
tableName: routine_task
columns:
- column: { name: id, type: BIGINT, autoIncrement: true, constraints: { primaryKey: true, nullable: false } }
- column: { name: family_id, type: BIGINT, constraints: { nullable: false } }
- column: { name: label_es, type: VARCHAR(120), constraints: { nullable: false } }
- column: { name: label_ca, type: VARCHAR(120), constraints: { nullable: false } }
- column: { name: icon, type: VARCHAR(16), constraints: { nullable: false } }
- column: { name: color, type: VARCHAR(9), constraints: { nullable: false } }
- column: { name: coins_reward, type: INT }
- addForeignKeyConstraint:
baseTableName: routine_task
baseColumnNames: family_id
referencedTableName: family
referencedColumnNames: id
constraintName: fk_routine_task_family
onDelete: CASCADE
rollback:
- dropTable:
tableName: routine_task
# Poblar el catálogo deduplicando las rutinas existentes por (familia + definición).
# La familia se obtiene a través del niño dueño de la rutina.
- changeSet:
id: 301-seed-routine-task-from-existing
author: recordalexia
changes:
- sql:
sql: >-
INSERT INTO routine_task (family_id, label_es, label_ca, icon, color, coins_reward)
SELECT DISTINCT c.family_id, ar.label_es, ar.label_ca, ar.icon, ar.color, NULL
FROM afternoon_routine ar
JOIN child c ON c.id = ar.child_id;
rollback:
- sql:
sql: DELETE FROM routine_task;
- changeSet:
id: 302-add-routine-task-id
author: recordalexia
changes:
- addColumn:
tableName: afternoon_routine
columns:
- column: { name: routine_task_id, type: BIGINT }
rollback:
- dropColumn:
tableName: afternoon_routine
columnName: routine_task_id
# Enlazar cada asignación con su rutina de catálogo por (familia + definición exacta).
- changeSet:
id: 303-link-routine-task
author: recordalexia
changes:
# Subconsulta correlacionada (SQL estándar, portable a H2 y PostgreSQL).
- sql:
sql: >-
UPDATE afternoon_routine ar
SET routine_task_id = (
SELECT rt.id FROM routine_task rt
WHERE rt.family_id = (SELECT c.family_id FROM child c WHERE c.id = ar.child_id)
AND rt.label_es = ar.label_es
AND rt.label_ca = ar.label_ca
AND rt.icon = ar.icon
AND rt.color = ar.color
);
rollback: [] # no-op: el enlace se deshace al recrear las columnas en 304
# Cerrar: NOT NULL + FK, y eliminar las columnas de definición ya migradas.
- changeSet:
id: 304-finalize-afternoon-routine
author: recordalexia
changes:
- addNotNullConstraint:
tableName: afternoon_routine
columnName: routine_task_id
columnDataType: BIGINT
- addForeignKeyConstraint:
baseTableName: afternoon_routine
baseColumnNames: routine_task_id
referencedTableName: routine_task
referencedColumnNames: id
constraintName: fk_routine_assignment_task
- dropColumn: { tableName: afternoon_routine, columnName: label_es }
- dropColumn: { tableName: afternoon_routine, columnName: label_ca }
- dropColumn: { tableName: afternoon_routine, columnName: icon }
- dropColumn: { tableName: afternoon_routine, columnName: color }
rollback:
# Recrear las columnas de definición y repoblarlas desde el catálogo.
- addColumn:
tableName: afternoon_routine
columns:
- column: { name: label_es, type: VARCHAR(120) }
- column: { name: label_ca, type: VARCHAR(120) }
- column: { name: icon, type: VARCHAR(16) }
- column: { name: color, type: VARCHAR(9) }
- sql:
sql: >-
UPDATE afternoon_routine ar
SET label_es = (SELECT rt.label_es FROM routine_task rt WHERE rt.id = ar.routine_task_id),
label_ca = (SELECT rt.label_ca FROM routine_task rt WHERE rt.id = ar.routine_task_id),
icon = (SELECT rt.icon FROM routine_task rt WHERE rt.id = ar.routine_task_id),
color = (SELECT rt.color FROM routine_task rt WHERE rt.id = ar.routine_task_id);
- dropForeignKeyConstraint:
baseTableName: afternoon_routine
constraintName: fk_routine_assignment_task

View File

@@ -7,12 +7,14 @@ import es.asepeyo.recordalexia.domain.AfternoonRoutine;
import es.asepeyo.recordalexia.domain.Child; import es.asepeyo.recordalexia.domain.Child;
import es.asepeyo.recordalexia.domain.DailyTask; import es.asepeyo.recordalexia.domain.DailyTask;
import es.asepeyo.recordalexia.domain.Family; import es.asepeyo.recordalexia.domain.Family;
import es.asepeyo.recordalexia.domain.RoutineTask;
import es.asepeyo.recordalexia.domain.Slot; import es.asepeyo.recordalexia.domain.Slot;
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry; import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
import es.asepeyo.recordalexia.repository.ActivityRepository; import es.asepeyo.recordalexia.repository.ActivityRepository;
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository; import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
import es.asepeyo.recordalexia.repository.ChildRepository; import es.asepeyo.recordalexia.repository.ChildRepository;
import es.asepeyo.recordalexia.repository.FamilyRepository; import es.asepeyo.recordalexia.repository.FamilyRepository;
import es.asepeyo.recordalexia.repository.RoutineTaskRepository;
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository; import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
import java.time.DayOfWeek; import java.time.DayOfWeek;
import java.time.LocalDate; import java.time.LocalDate;
@@ -32,6 +34,7 @@ class DayGenerationServiceTest {
@Autowired private ActivityRepository activityRepository; @Autowired private ActivityRepository activityRepository;
@Autowired private WeeklyTemplateEntryRepository templateRepository; @Autowired private WeeklyTemplateEntryRepository templateRepository;
@Autowired private AfternoonRoutineRepository routineRepository; @Autowired private AfternoonRoutineRepository routineRepository;
@Autowired private RoutineTaskRepository routineTaskRepository;
@Autowired private FamilyRepository familyRepository; @Autowired private FamilyRepository familyRepository;
private Family family; private Family family;
@@ -48,7 +51,8 @@ class DayGenerationServiceTest {
actividad.setFamily(family); actividad.setFamily(family);
Activity mates = activityRepository.save(actividad); Activity mates = activityRepository.save(actividad);
templateRepository.save(new WeeklyTemplateEntry(nino, dia, mates, 0)); templateRepository.save(new WeeklyTemplateEntry(nino, dia, mates, 0));
routineRepository.save(new AfternoonRoutine(nino, dia, "Deberes", "Deures", "📝", "#F2A65A", 0)); RoutineTask deberes = routineTaskRepository.save(routineTask("Deberes", "Deures", "📝", "#F2A65A"));
routineRepository.save(new AfternoonRoutine(nino, dia, deberes, 0));
List<DailyTask> primera = dayGenerationService.generateIfAbsent(nino.getId(), fecha); List<DailyTask> primera = dayGenerationService.generateIfAbsent(nino.getId(), fecha);
@@ -63,6 +67,45 @@ class DayGenerationServiceTest {
assertThat(segunda).hasSize(2); assertThat(segunda).hasSize(2);
} }
@Test
void aplicaLaCascadaDeMonedasDeLaRutina() {
family = familyRepository.save(new Family("coins@x.com", "h", "C", "p"));
LocalDate fecha = LocalDate.of(2026, 6, 22);
DayOfWeek dia = fecha.getDayOfWeek();
// El niño rinde 10 por tarea por defecto.
Child nino = childRepository.save(nuevoNino());
nino.setCoinsPerTask(10);
// (a) Override por día (8) gana al catálogo (5) y al niño (10).
RoutineTask conDefecto = routineTaskRepository.save(routineTask("Merienda", "Berenar", "🥪", "#F4C95D"));
conDefecto.setCoinsReward(5);
AfternoonRoutine conOverride = new AfternoonRoutine(nino, dia, conDefecto, 0);
conOverride.setCoinsReward(8);
routineRepository.save(conOverride);
// (b) Sin override → vale el del catálogo (5).
RoutineTask soloCatalogo = routineTaskRepository.save(routineTask("Leer", "Llegir", "📖", "#F2A65A"));
soloCatalogo.setCoinsReward(5);
routineRepository.save(new AfternoonRoutine(nino, dia, soloCatalogo, 1));
// (c) Sin override ni catálogo → cae a coinsPerTask del niño (10).
RoutineTask sinNada = routineTaskRepository.save(routineTask("Recoger", "Recollir", "🧹", "#7FBF6B"));
routineRepository.save(new AfternoonRoutine(nino, dia, sinNada, 2));
List<DailyTask> tareas = dayGenerationService.generateIfAbsent(nino.getId(), fecha);
assertThat(tareas).filteredOn(t -> t.getSlot() == Slot.AFTERNOON)
.extracting(DailyTask::getCoinsReward)
.containsExactly(8, 5, 10);
}
private RoutineTask routineTask(String es, String ca, String icon, String color) {
RoutineTask rt = new RoutineTask(es, ca, icon, color);
rt.setFamily(family);
return rt;
}
private Child nuevoNino() { private Child nuevoNino() {
Child c = new Child(); Child c = new Child();
c.setFamily(family); c.setFamily(family);

View File

@@ -14,6 +14,7 @@ import es.asepeyo.recordalexia.domain.Child;
import es.asepeyo.recordalexia.domain.Family; import es.asepeyo.recordalexia.domain.Family;
import es.asepeyo.recordalexia.domain.FamilySession; import es.asepeyo.recordalexia.domain.FamilySession;
import es.asepeyo.recordalexia.domain.Reward; import es.asepeyo.recordalexia.domain.Reward;
import es.asepeyo.recordalexia.domain.RoutineTask;
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry; import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
import es.asepeyo.recordalexia.repository.ActivityRepository; import es.asepeyo.recordalexia.repository.ActivityRepository;
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository; import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
@@ -21,6 +22,7 @@ import es.asepeyo.recordalexia.repository.ChildRepository;
import es.asepeyo.recordalexia.repository.FamilyRepository; import es.asepeyo.recordalexia.repository.FamilyRepository;
import es.asepeyo.recordalexia.repository.FamilySessionRepository; import es.asepeyo.recordalexia.repository.FamilySessionRepository;
import es.asepeyo.recordalexia.repository.RewardRepository; import es.asepeyo.recordalexia.repository.RewardRepository;
import es.asepeyo.recordalexia.repository.RoutineTaskRepository;
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository; import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
import es.asepeyo.recordalexia.security.SessionAuthFilter; import es.asepeyo.recordalexia.security.SessionAuthFilter;
import java.time.DayOfWeek; import java.time.DayOfWeek;
@@ -46,6 +48,7 @@ class TodayFlowIT {
@Autowired private ActivityRepository activityRepository; @Autowired private ActivityRepository activityRepository;
@Autowired private WeeklyTemplateEntryRepository templateRepository; @Autowired private WeeklyTemplateEntryRepository templateRepository;
@Autowired private AfternoonRoutineRepository routineRepository; @Autowired private AfternoonRoutineRepository routineRepository;
@Autowired private RoutineTaskRepository routineTaskRepository;
@Autowired private RewardRepository rewardRepository; @Autowired private RewardRepository rewardRepository;
@Autowired private FamilyRepository familyRepository; @Autowired private FamilyRepository familyRepository;
@Autowired private FamilySessionRepository sessionRepository; @Autowired private FamilySessionRepository sessionRepository;
@@ -62,7 +65,10 @@ class TodayFlowIT {
mates.setFamily(family); mates.setFamily(family);
mates = activityRepository.save(mates); mates = activityRepository.save(mates);
templateRepository.save(new WeeklyTemplateEntry(nino, hoy, mates, 0)); templateRepository.save(new WeeklyTemplateEntry(nino, hoy, mates, 0));
routineRepository.save(new AfternoonRoutine(nino, hoy, "Deberes", "Deures", "📝", "#F2A65A", 0)); RoutineTask deberes = new RoutineTask("Deberes", "Deures", "📝", "#F2A65A");
deberes.setFamily(family);
deberes = routineTaskRepository.save(deberes);
routineRepository.save(new AfternoonRoutine(nino, hoy, deberes, 0));
Reward premio = new Reward("Tablet", "Tauleta", "🎮", "#5B8DEF", 5); Reward premio = new Reward("Tablet", "Tauleta", "🎮", "#5B8DEF", 5);
premio.setFamily(family); premio.setFamily(family);
premio = rewardRepository.save(premio); premio = rewardRepository.save(premio);