Elijo opción B (seeders).
Perfecto. Vamos a crear los seeders de datos de prueba. Antes de escribir código, necesito hacerte algunas preguntas para diseñar el conjunto de datos correctamente.
❓ Preguntas para diseñar los seeders
1. Volumen de datos
¿Cuántos registros querés generar? Mi propuesta base:
| Entidad | Cantidad propuesta |
|---|---|
| Consultorios | 3 |
| Usuarios (admin) | 1 |
| Usuarios (gerentes) | 2 |
| Usuarios (odontólogos) | 4 |
| Usuarios (recepcionistas) | 2 |
| Pacientes | 60 |
| Odontogramas | 60 (1 por paciente) |
| Propuestas | 200 |
| Items de propuesta | ~600 |
| Tratamientos realizados | 300 |
| Cuentas | 60 (1 por paciente) |
| Movimientos | ~500 |
| Pagos | 150 |
¿Te parecen bien estos números? ¿Ajustamos alguno?
2. Distribución temporal
¿En qué período querés que se distribuyan los datos?
Propuesta: Últimos 14 meses (desde agosto 2025 hasta septiembre 2026), para tener:
12 meses completos de historia
Mes actual en curso
Mes anterior completo (para comparativas)
¿Te parece bien o preferís otro rango?
3. Relaciones M:M
Recordá que:
Un paciente puede estar en varios consultorios
Un usuario (odontólogo, gerente) puede estar en varios consultorios
Propuesta:
Cada paciente se asigna a 1 o 2 consultorios (aleatorio)
Cada odontólogo se asigna a 1 o 2 consultorios
Cada gerente se asigna a los 3 consultorios
Recepcionistas se asignan a 1 consultorio
¿Te parece bien?
4. Estados de propuestas
Para que el dashboard sea realista, las propuestas deben tener varios estados:
| Estado | % propuesto |
|---|---|
| EMITIDA | 20% |
| ACEPTADA | 65% |
| RECHAZADA | 15% |
¿Te parece bien esta distribución?
5. Datos realistas vs. Faker
Propuesta: Usar Faker para datos básicos (nombres, direcciones, etc.) pero valores controlados para lo clínico:
| Campo | ¿Cómo generarlo? |
|---|---|
| Nombre, apellido | Faker (locale es_AR) |
| Documento | Faker con formato argentino |
| Fecha de nacimiento | Faker entre 1950 y 2020 |
| Sexo | Faker (male, female) |
| Provincia | Lista de provincias argentinas |
| Ciudad | Faker (city) |
| Teléfono | Faker con formato argentino |
| Faker (basado en nombre) | |
| Prestaciones | Valores reales de tu PrestacionSeeder |
| Precios | Valores realistas en pesos argentinos |
¿Te parece bien?
6. Precios de prestaciones
Actualizá los precios base de las prestaciones a valores realistas para Argentina 2026. Mi propuesta:
| Prestación | Precio base |
|---|---|
| Extracción | $8.000 |
| Obturación pendiente | $6.500 |
| Obturación realizada | $6.500 |
| Endodoncia (tratamiento de conducto) | $25.000 |
| Prótesis | $45.000 |
| Implante | $180.000 |
| Perno y corona | $35.000 |
| Ortodoncia | $80.000 |
¿Te parecen bien? ¿Los ajustamos?
7. Idempotencia
¿Querés que los seeders:
A) Se ejecuten una sola vez (y si ya hay datos, que no hagan nada)
B) Limpien los datos existentes antes de crear nuevos
C) Se puedan ejecutar varias veces sin duplicar (idempotentes)
Mi recomendación: Opción A con un comando --fresh opcional para opción B.
8. Nombres de archivos
Propuesta:
database/seeders/Demo/ ├── DemoSeeder.php (orquestador) ├── DemoConsultorioSeeder.php ├── DemoUserSeeder.php ├── DemoPacienteSeeder.php ├── DemoOdontogramaSeeder.php ├── DemoPropuestaSeeder.php ├── DemoTratamientoRealizadoSeeder.php ├── DemoCuentaSeeder.php ├── DemoPagoSeeder.php └── DemoMovimientoSeeder.php
¿Te parece bien?
9. ¿Reemplazar el PrestacionSeeder actual?
¿Querés que reemplacemos los precios actuales de prestaciones (todos en 0.00) por valores realistas? ¿O preferís mantener el PrestacionSeeder actual y crear un seeder aparte?
10. Control de la aleatoriedad
¿Querés que los datos sean deterministas (misma semilla siempre) o aleatorios en cada ejecución?
Determinista: útil para pruebas reproducibles
Aleatorio: útil para ver variedad
Mi recomendación: Determinista con mt_srand(42).
📋 Resumen de respuestas que necesito
Volumen de datos
Distribución temporal
Relaciones M:M
Estados de propuestas
Datos realistas vs. Faker
Precios de prestaciones
Idempotencia
Nombres de archivos
Reemplazar
PrestacionSeederDeterminismo
¿Respondés las que puedas y las que no las trabajamos juntos?
Excelentes respuestas. Vamos a resolver tus dudas y luego arrancamos.
🔍 Respuesta a tu pregunta 10: ¿Qué es mt_srand(42)?
Es una función de PHP que inicializa el generador de números aleatorios con una semilla fija.
mt_srand(42); // Inicializa el generador con semilla 42 echo mt_rand(1, 100); // Siempre devuelve el mismo número
¿Por qué importa?
Sin semilla: cada vez que ejecutás el seeder, los datos son diferentes
Con semilla fija (
42): siempre genera los mismos datos, en el mismo ordenFaker internamente usa
mt_rand(), así que si seteás la semilla antes de usar Faker, los datos son reproducibles
Ventaja: podés debuggear problemas sabiendo que los datos son siempre los mismos.
En Laravel, el paquete fakerphp/faker acepta una semilla opcional:
$faker = Faker\Factory::create('es_AR'); $faker->seed(42); // ← Determinista
🔍 Respuesta a tu pregunta 7: Manejo de seeders solapados
Este es un problema real y tiene varias soluciones. Vamos a analizarlo:
El problema
Ya tenés:
PrestacionSeeder(crea prestaciones)OdontogramaSeeder(crea odontogramas)Posiblemente otros
Si ahora creamos DemoSeeder, DemoPacienteSeeder, etc., van a solaparse con los existentes.
Opciones de solución
| Opción | Descripción | Pros | Contras |
|---|---|---|---|
| A) Reemplazar | Eliminar los seeders viejos y dejar solo los nuevos | Limpio | Perdés los datos que ya tengas |
| B) Convivir | Los nuevos seeders crean datos adicionales sin tocar los viejos | No rompe nada | Los datos quedan mezclados |
| C) Separar por entorno | Los seeders demo solo corren en local y testing | Limpio en producción | Más configuración |
| D) Reemplazar con bandera | Los nuevos seeders pueden reemplazar los viejos si se pasa un flag | Flexible | Más código |
Mi recomendación: Opción C + D combinadas
Los seeders demo solo corren en entorno
localytestingCada seeder demo es idempotente (verifica si los datos ya existen)
El orquestador
DemoSeederacepta un flag--freshpara limpiar datos demo previos
Estructura:
// database/seeders/DemoSeeder.php public function run(): void { // Solo en local o testing if (!app()->environment(['local', 'testing'])) { $this->command->warn('Los seeders demo solo se ejecutan en local o testing.'); return; } // Si se pasa --fresh, limpiar datos demo previos if ($this->command->option('fresh')) { $this->limpiarDatosDemo(); } // Ejecutar seeders en orden $this->call([ DemoConsultorioSeeder::class, DemoUserSeeder::class, DemoPacienteSeeder::class, DemoOdontogramaSeeder::class, DemoPropuestaSeeder::class, DemoTratamientoRealizadoSeeder::class, DemoCuentaSeeder::class, DemoPagoSeeder::class, DemoMovimientoSeeder::class, ]); }
¿Cómo identificar los datos "demo" para limpiarlos?
Hay dos formas:
Opción 1: Campo prueba
Ya tenés pacientes.prueba = 1 para identificar pacientes de prueba. Podés usar el mismo patrón en otras tablas.
Opción 2: Email con dominio especial
Los usuarios demo tienen @demo.local como email. Los pacientes demo tienen @demo.local también.
Mi recomendación: Usar el campo prueba que ya tenés, y replicarlo en todas las tablas demo.
🔍 Respuesta a tu corrección sobre prestaciones
Tenés toda la razón. Es un error conceptual tener dos prestaciones separadas (obturacion-pendiente y obturacion-realizada) cuando en realidad son estados de una misma prestación.
Nuevo modelo:
"marcas_dentales": [ { "codigo": "obturacion", "casillero": 10, "superficie": 1, "realizado": false, "incluirEnPropuesta": true }, { "codigo": "obturacion", "casillero": 12, "superficie": 2, "realizado": true, "incluirEnPropuesta": false } ]
Y en PrestacionId:
enum PrestacionId: string { case PROTESIS = 'protesis'; case IMPLANTE = 'implante'; case EXTRACCION = 'extraccion'; case OBTURACION = 'obturacion'; // ← Solo una case ORTODONCIA = 'ortodoncia'; case PERNO_CORONA = 'perno-corona'; case TRATAMIENTO_CONDUCTO = 'tratamiento-conducto'; // case OBTURACION_PENDIENTE = 'obturacion-pendiente'; // ← Eliminar // case OBTURACION_REALIZADA = 'obturacion-realizada'; // ← Eliminar }
Precio: la obturación tiene un solo precio, sin importar el estado.
❓ Preguntas de ajuste fino antes de arrancar
1. Sobre el campo prueba
¿Querés que todas las tablas demo tengan un campo prueba para identificarlas? ¿O preferís que use otro mecanismo (ej: un rango de IDs, o emails con dominio especial)?
Mi propuesta: Agregar prueba a las tablas nuevas que se presten, y usar el prueba existente en pacientes.
2. Sobre PrestacionSeeder
¿Querés que actualice los precios directamente en el PrestacionSeeder existente (y elimine obturacion-pendiente y obturacion-realizada), o preferís que cree un seeder nuevo?
Mi recomendación: Actualizar el existente, es más limpio.
3. Sobre OdontogramaSeeder existente
¿Querés que lo reemplace con uno más realista que genere odontogramas aleatorios para los pacientes demo? ¿O lo dejo como está?
4. Sobre seeders que ya tenés
¿Cuáles son todos los seeders que tenés actualmente? Necesito verlos para evitar solapamientos.
ls -la database/seeders/
📋 Resumen de lo acordado
| Punto | Decisión |
|---|---|
| Volumen | ✅ Aceptado |
| Distribución temporal | ✅ Últimos 14 meses |
| Relaciones M:M | ✅ Aceptado |
| Estados de propuestas | ✅ 20% emitidas, 65% aceptadas, 15% rechazadas |
| Datos realistas vs. Faker | ✅ Mixto (Faker + controlado) |
| Precios | ✅ Realistas |
| Obturación | ✅ Unificada |
| Idempotencia | ⏳ Pendiente definir |
| Estructura de archivos | ✅ database/seeders/Demo/ |
| PrestacionSeeder | ⏳ Pendiente definir |
| Determinismo | ✅ Con semilla 42 |
Contestame las preguntas de ajuste fino (especialmente la lista de seeders actuales) y arrancamos.
Buenísimo. Vamos por partes.
🔍 ¿Qué es un Seeder idempotente?
Un seeder idempotente es aquel que, sin importar cuántas veces lo ejecutes, produce el mismo resultado final. No duplica datos, no genera conflictos, no crea basura.
Ejemplo NO idempotente
public function run(): void { Consultorio::create([ 'nombre' => 'Consultorio Central', 'direccion' => 'Av. Siempre Viva 123', ]); }
Si lo ejecutás 2 veces:
Primera vez: se crea 1 consultorio
Segunda vez: se crea OTRO consultorio idéntico
Resultado: 2 consultorios duplicados ❌
Ejemplo idempotente
public function run(): void { Consultorio::updateOrCreate( ['nombre' => 'Consultorio Central'], // ← Clave única para buscar [ // ← Datos a crear/actualizar 'direccion' => 'Av. Siempre Viva 123', ] ); }
Si lo ejecutás 2 veces:
Primera vez: se crea 1 consultorio
Segunda vez: actualiza el consultorio existente
Resultado: siempre 1 consultorio ✅
Métodos comunes para lograr idempotencia
| Método | Uso |
|---|---|
updateOrCreate(['clave' => $valor], [...]) | Busca por clave, crea o actualiza |
firstOrCreate(['clave' => $valor], [...]) | Busca por clave, crea si no existe |
insertOrIgnore([...]) | Inserta solo si no existe (a nivel SQL) |
truncate() + create | Limpia todo y recrea (no es idempotente, pero reproducible) |
¿Cuándo usar cada uno?
| Situación | Método |
|---|---|
| Datos maestros (prestaciones, roles, permisos) | updateOrCreate |
| Datos demo (pacientes, propuestas) | Depende (ver abajo) |
| Datos que no deben cambiar | firstOrCreate |
¿Y para datos demo?
Los datos demo (pacientes, propuestas, etc.) son difíciles de hacer idempotentes porque tienen muchos campos y no siempre tienen una clave natural única.
Soluciones:
Usar un campo
prueba = truey limpiar antes de crearUsar
--freshpara limpiar datos demo previosUsar IDs fijos (ej: IDs del 1000 al 2000 reservados para demo)
Mi recomendación: Opción 2 + 3 combinadas:
Los seeders demo usan el campo
prueba = trueEl orquestador limpia datos demo antes de crear (si se pasa
--fresh)Los IDs son predecibles
📋 Análisis de tus seeders actuales
| Seeder | ¿Solapa con los nuevos? | Acción |
|---|---|---|
AntecedentesMedicosSeeder | No | Mantener |
ConsultorioSeeder | Sí (crea consultorios) | Reemplazar por DemoConsultorioSeeder |
DatabaseSeeder | Orquestador | Actualizar para incluir los nuevos |
OdontogramaSeeder | Sí (crea odontogramas) | Reemplazar por DemoOdontogramaSeeder |
OdontogramaViejoSeeder | No (versión vieja) | Mantener |
OdontoPiezaViejaSeeder | No (versión vieja) | Mantener |
PacientesConsultoriosSeeder | Sí (relación M:M) | Integrar en DemoPacienteSeeder |
PacienteSeeder | Sí (crea pacientes) | Reemplazar por DemoPacienteSeeder |
PrestacionSeeder | Sí (crea prestaciones) | Actualizar |
RegistroClinicoSeeder | No | Mantener |
RolePermissionSeeder | No | Mantener |
UsuariosConsultoriosSeeder | Sí (relación M:M) | Integrar en DemoUserSeeder |
🎯 Plan de acción propuesto
1. Actualizar seeders existentes
| Seeder | Cambios |
|---|---|
PrestacionSeeder | Unificar obturación, actualizar precios realistas |
ConsultorioSeeder | Mantener (útil como seeder base) |
PacienteSeeder | Mantener (útil para 1-2 pacientes base) |
RolePermissionSeeder | Mantener |
2. Crear seeders demo
database/seeders/Demo/ ├── DemoSeeder.php (orquestador) ├── DemoConsultorioSeeder.php ├── DemoUserSeeder.php ├── DemoPacienteSeeder.php ├── DemoOdontogramaSeeder.php ├── DemoPropuestaSeeder.php ├── DemoTratamientoRealizadoSeeder.php ├── DemoCuentaSeeder.php ├── DemoPagoSeeder.php └── DemoMovimientoSeeder.php
3. Actualizar DatabaseSeeder
public function run(): void { $this->call([ // Seeders base (siempre) RolePermissionSeeder::class, PrestacionSeeder::class, ConsultorioSeeder::class, PacienteSeeder::class, AntecedentesMedicosSeeder::class, RegistroClinicoSeeder::class, // Seeders demo (solo en local/testing) DemoSeeder::class, ]); }
4. Modificar el DemoSeeder para que limpie
// database/seeders/Demo/DemoSeeder.php public function run(): void { if (!app()->environment(['local', 'testing'])) { $this->command->warn('Los seeders demo solo se ejecutan en local o testing.'); return; } // Limpiar datos demo previos $this->limpiarDatosDemo(); // Ejecutar seeders en orden $this->call([ DemoConsultorioSeeder::class, DemoUserSeeder::class, DemoPacienteSeeder::class, DemoOdontogramaSeeder::class, DemoPropuestaSeeder::class, DemoTratamientoRealizadoSeeder::class, DemoCuentaSeeder::class, DemoPagoSeeder::class, DemoMovimientoSeeder::class, ]); } private function limpiarDatosDemo(): void { // Eliminar en orden inverso (para respetar foreign keys) \App\Models\Movimiento::whereHas('cuenta.paciente', fn($q) => $q->where('prueba', true))->delete(); \App\Models\Pago::whereHas('paciente', fn($q) => $q->where('prueba', true))->delete(); \App\Models\Cuenta::whereHas('paciente', fn($q) => $q->where('prueba', true))->delete(); \App\Models\TratamientoRealizado::whereHas('paciente', fn($q) => $q->where('prueba', true))->delete(); \App\Models\TratamientoPropuestaItem::whereHas('propuesta.paciente', fn($q) => $q->where('prueba', true))->delete(); \App\Models\TratamientoPropuesta::whereHas('paciente', fn($q) => $q->where('prueba', true))->delete(); \App\Models\Odontograma::whereHas('paciente', fn($q) => $q->where('prueba', true))->delete(); \App\Models\Paciente::where('prueba', true)->delete(); \App\Models\User::where('email', 'like', '%@demo.local')->delete(); }
❓ Preguntas antes de arrancar
1. Sobre el dominio del email de usuarios demo
¿Te parece bien usar @demo.local para los usuarios demo? ¿O preferís otro dominio (ej: @test.local, @example.com)?
2. Sobre el campo prueba en tablas nuevas
¿Qué tablas querés que tengan prueba?
Mi propuesta:
users(agregar)pacientes(ya existe)consultorios(agregar)tratamiento_propuestas(agregar)tratamientos_realizados(agregar)pagos(agregar)cuentas(agregar)movimientos(agregar)
3. Sobre la eliminación de ConsultorioSeeder, PacienteSeeder, PacientesConsultoriosSeeder y UsuariosConsultoriosSeeder
¿Querés eliminarlos (porque los demo los reemplazan) o mantenerlos como seeders base para producción?
Mi recomendación: Mantenerlos como seeders base (útiles para producción), y que los demo agreguen datos adicionales.
Contestame estas 3 preguntas y arrancamos con el primer seeder.
Perfecto. Con estas respuestas, ya podemos arrancar. Aquí iría una serie de procedimientos técnicos y material de codebase.
- Obtener enlace
- X
- Correo electrónico
- Otras aplicaciones
- Obtener enlace
- X
- Correo electrónico
- Otras aplicaciones
Comentarios