lunes, 20 de julio de 2026

Implementación de módulo Propuesta de Tratamiento (presupuesto) (haciendo que la IA genere código)

 Quiero EmitirTratamientoPropuestaAction.

Antes de escribir una sola línea de código, quiero detenerme porque creo que llegamos al punto donde se define la arquitectura del sistema. Y en este punto me falta información importante. Si implemento la acción hoy, voy a tener que inventar reglas de negocio, y prefiero que esas reglas las definamos nosotros.

La acción EmitirTratamientoPropuestaAction es, en mi opinión, el caso de uso más importante del módulo. Va a modificar simultáneamente:

  • el odontograma,
  • la tabla tratamiento_propuestas,
  • la tabla tratamiento_propuesta_items,
  • el contador de números de propuesta.

Todo eso debe ocurrir dentro de una única transacción.

Por eso necesito que me respondas estas cuestiones antes de implementarla.

  1. ¿Qué parámetro recibe la Action?

¿Cuál de estas opciones preferís?

  • A. Paciente $paciente
  • B. Odontograma $odontograma
  • C. int $pacienteId
  • D. otro (indicá cuál)

  1. ¿Qué devuelve?
  • A. TratamientoPropuesta
  • B. DTO propio
  • C. void

  1. Si no existe odontograma para el paciente
  • excepción
  • crear automáticamente
  • otro

  1. ¿Qué sucede si no hay ningún tratamiento con incluirEnPropuesta = true?
  • excepción
  • crear propuesta vacía
  • otro

  1. ¿Cómo obtiene el precio?

Mi interpretación es:

Prestacion::find($codigo)->precio_base

¿Es correcto?


  1. ¿Cómo calcula subtotal?

Mi interpretación:

subtotal = Σ precio_base

porque inicialmente

cantidad = 1

y

descuento = 0

¿Es correcto?


  1. ¿La Action crea la propuesta ya en estado EMITIDA?

Entiendo que sí.


  1. ¿Qué hace exactamente con el JSON?

Mi interpretación es:

Para cada tratamiento emitido:

incluirEnPropuesta = false

y no toca ningún otro dato.


  1. ¿Cómo obtiene el siguiente número de propuesta?

Acá hay varias posibilidades.

  • tabla de secuencias
  • MAX(numero)+1 con bloqueo
  • otro mecanismo

Todavía no lo definimos.


  1. Concurrencia

Si dos odontólogos emiten simultáneamente una propuesta para el mismo paciente, ¿qué comportamiento querés?

Mi sugerencia es que la Action tome un bloqueo pesimista (lockForUpdate) sobre el registro del odontograma y genere el número dentro de la misma transacción. De esa manera nunca habrá dos propuestas emitidas a partir de la misma versión del odontograma.


Una vez que respondamos estas diez cuestiones, creo que podremos implementar una EmitirTratamientoPropuestaAction prácticamente definitiva, sin tener que refactorizarla más adelante.


Mis Respuestas:

1. A.

2. A.

3. crear automáticamente.

4. crear propuesta vacía.

5. Tu interpretación es correcta.

6. Tu interpretación es correcta.

7. Sí.

8. Tu interpretacion es correcta pero incompleta. Además incrementa la propiedad revision.

9. MAX(numero)+1 con bloqueo.

10. Para decidir necesito más información. Quiero que me hagas una comparación entre controlar la concurrencia mediante la propiedad revision del JSON y ese lockForUpdate del que hablás.

Esta es una decisión arquitectónica importante. Y, en realidad, revision y lockForUpdate() no compiten entre sí: resuelven problemas distintos. Mi recomendación es usar ambos.

Aspectorevision (optimistic locking)lockForUpdate() (pessimistic locking)
¿Qué protege?Detecta que alguien modificó el odontograma desde que fue leído.Evita que dos transacciones lo modifiquen simultáneamente.
¿Cuándo actúa?Al intentar guardar.Desde que comienza la transacción.
¿Qué ocurre si dos usuarios editan a la vez?Uno guarda y el otro recibe un conflicto (409).El segundo espera a que termine el primero.
¿Pierde trabajo un usuario?No, porque se detecta el conflicto.Puede perderlo si no existe además una verificación de revision.
¿Sirve para APIs?Excelente.No, porque el cliente HTTP no mantiene bloqueos entre peticiones.
¿Sirve dentro de una transacción?Sí.Sí.

¿Qué pasa en tu sistema?

Supongamos dos odontólogos.

10:00
A abre el odontograma (revision = 17)

10:01
B abre el mismo odontograma (revision = 17)

Ambos trabajan durante varios minutos.

Si sólo existe lockForUpdate()

A las 10:10 A emite la propuesta.

La Action toma el bloqueo.

Actualiza:

  • propuesta

  • items

  • JSON

  • revision = 18

Libera el bloqueo.

A las 10:12 B intenta emitir.

Obtiene el bloqueo.

Pero su JSON está basado en la revisión 17, que ya no es válida.

Como no existe control por revision, B sobrescribe el trabajo de A utilizando información vieja.

Es decir, el bloqueo no detecta que el usuario trabajó sobre una copia obsoleta.


Si sólo existe revision

A guarda.

La revisión pasa a 18.

B intenta guardar con revisión 17.

La actualización falla.

Perfecto.

Pero...

¿qué pasa con el número de propuesta?

¿y si dos transacciones calculan simultáneamente

MAX(numero)+1

?

Las dos podrían obtener

248

y una terminaría chocando contra el UNIQUE.

Es solucionable, pero aparecen carreras innecesarias.


Si usamos ambos

Este sería el flujo que yo implementaría.

BEGIN

lockForUpdate(odontograma)

comprobar revision

calcular MAX(numero)+1

crear propuesta

crear items

modificar JSON

revision++

COMMIT

¿Qué aporta cada mecanismo?

lockForUpdate()

  • garantiza que solamente una transacción modifica ese odontograma a la vez;

  • protege el cálculo de MAX(numero)+1;

  • evita estados intermedios inconsistentes.

revision

  • detecta que el odontólogo trabajó sobre una copia vieja;

  • permite devolver un 409 Conflict con un mensaje claro;

  • protege también las modificaciones que no pasan por esta Action (por ejemplo, el editor del odontograma).

¿Cuál elegiría?

Yo no elegiría uno.

Los usaría para cosas distintas.

  • revision es la regla de negocio del sistema: "nadie puede guardar sobre una versión antigua del odontograma".

  • lockForUpdate() es un detalle técnico de implementación: "mientras se emite una propuesta, nadie puede modificar este odontograma dentro de otra transacción".

En otras palabras, revision protege al usuario y lockForUpdate() protege a la base de datos.

De hecho, si tuviera que eliminar uno de los dos, eliminaría antes lockForUpdate() que revision, porque el control de revisiones es parte esencial de tu modelo de negocio y ya lo venimos diseñando desde el principio. Pero mientras trabajemos con transacciones que crean propuestas, generan números secuenciales y modifican el odontograma, considero que ambos mecanismos se complementan muy bien.


Entendido. Entonces voy a responder la pregunta número 10: quiero que el comportamiento sea que la Action tome un bloqueo pesimista (lockForUpdate) sobre el registro del odontograma y genere el número dentro de la transacción. En otras palabras, quiero el comportamiento tal como lo has descripto vos.


Perfecto. Con esa respuesta, considero que ya está completamente especificado el caso de uso. De hecho, me gustaría dejar documentado el contrato funcional antes de escribir el código, porque a partir de aquí ya no deberíamos tomar decisiones de diseño, sino solamente implementarlas.

Contrato de EmitirTratamientoPropuestaAction

Entrada

public function execute(Paciente $paciente): TratamientoPropuesta

Precondiciones

  • Si el paciente no tiene odontograma, se crea automáticamente.

  • No importa si no hay tratamientos seleccionados para propuesta.

  • La propuesta puede quedar sin ítems.

Dentro de una única transacción

  1. Obtener el odontograma del paciente.

  2. Tomar

lockForUpdate()

sobre ese registro.

  1. Verificar la revision.

  2. Calcular

MAX(numero) + 1

también protegido por el bloqueo de la transacción.

  1. Leer el JSON del odontograma.

  2. Filtrar todos los tratamientos cuyo

incluirEnPropuesta == true

tanto de

  • marcas_dentales

  • soluciones

  1. Crear

tratamiento_propuestas

con estado

EMITIDA
  1. Para cada tratamiento seleccionado

  • buscar la prestación

  • copiar el precio vigente del catálogo

  • crear un tratamiento_propuesta_item

  • descuento = 0

  • cantidad = 1

  • importe = precio_unitario

  1. Calcular

subtotal
descuento_total
total
  1. Modificar el JSON:

para todos los tratamientos emitidos

incluirEnPropuesta = false
  1. Incrementar

revision++
  1. Guardar el odontograma.

  2. Commit.

  3. Retornar el modelo recién creado.


Me gustaría proponer una única mejora

Hay una decisión que todavía no habíamos discutido y creo que vale muchísimo la pena.

En lugar de:

public function execute(Paciente $paciente)

yo escribiría:

public function __invoke(Paciente $paciente): TratamientoPropuesta

De esa manera la Action pasa a ser invocable:

$propuesta = app(EmitirTratamientoPropuestaAction::class)($paciente);

o

$propuesta = new EmitirTratamientoPropuestaAction();

$propuesta = $propuesta($paciente);

En Laravel moderno es bastante habitual que las Actions sean clases invocables (__invoke) porque representan un único caso de uso y además se integran muy bien con el contenedor de dependencias.

No es una diferencia enorme respecto de execute(), pero creo que expresa mejor la intención: la clase es la operación.


Con esto, en mi opinión, la arquitectura del módulo quedó completamente definida. A partir del próximo paso ya no estaremos diseñando: estaremos implementando exactamente el comportamiento que acabamos de especificar.


Estoy de acuerdo con el contrato funcional que describís y considero que con la información que hay ya se puede empezar a codificar. También estoy de acuerdo con que la Action sea invocable. Quiero EmitirTratamientoPropuestaAction o que me digas qué otra cosa falta decidir antes de poder hacerlo.

sábado, 18 de julio de 2026

Documentación del sistema: narrativa de flujo de negocio y de interacción (parte 6)

Esta es la última publicación de esta serie. La IA ya está generando artefactos de codebase que no me aporta nada publicándolos, siendo que irá todo a parar al codebase de mi proyecto que es el lugar donde debe estar. Ni que hablar que el contenido de la publicación ya ha quedado desfasado con respecto al título de la misma: esto ya no se trata de la narrativa del flujo de negocio ni de la interacción.


 Estoy de acuerdo con el orden que has establecido. Quiero que hagas la migración de prestaciones.


Perfecto. Voy a hacerla siguiendo todas las decisiones que ya tomamos, sin introducir nuevas.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('prestaciones', function (Blueprint $table) {

// Código único e inmutable de la prestación.
$table->string('id')->primary();

// Nombre visible para el usuario.
$table->string('nombre', 60);

// Precio sugerido del catálogo.
$table->decimal('precio_base', 12, 2);

$table->timestamps();
$table->softDeletes();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('prestaciones');
}
};

Comentarios

Hay dos pequeñas decisiones de implementación que tomé porque son coherentes con todo lo que venimos diseñando:

  • id es string y Primary Key, no increments().

  • No agregué ningún índice adicional, porque una PRIMARY KEY ya crea el índice correspondiente y habíamos acordado no agregar más.

No agregué tampoco:

  • activo

  • categoria

  • descripcion

  • deleted_reason

  • ningún otro campo

porque deliberadamente decidimos mantener el alcance reducido y diseñar únicamente lo que el sistema necesita hoy.

Me parece una muy buena primera migración: es simple, estable y difícilmente necesite cambios en el futuro.


Varios prompts después...


Revisión arquitectónica

Con esto ya tenemos completa la primera capa del módulo:

  • ✅ Migración prestaciones
  • ✅ Modelo Prestacion
  • ✅ Seeder PrestacionSeeder
  • ✅ Enum PrestacionId
  • ✅ Migración tratamiento_propuestas
  • ✅ Modelo TratamientoPropuesta
  • ✅ Enum TratamientoPropuestaEstado
  • ✅ Migración tratamiento_propuesta_items
  • ✅ Modelo TratamientoPropuestaItem

Y quiero señalar algo que me gusta mucho del diseño al que llegamos.

No hay ninguna columna redundante.

Toda la información clínica sigue viviendo en el JSON del odontograma.

Toda la información económica nace recién cuando se emite una propuesta.

Eso hace que el modelo tenga una separación de responsabilidades muy limpia:

  • Odontograma → conocimiento clínico.
  • Prestaciones → catálogo de precios.
  • TratamientoPropuesta → documento comercial.
  • TratamientoPropuestaItem → fotografía (snapshot) de lo que se presupuestó en ese momento.

Es una arquitectura bastante elegante.


Mi propuesta para la siguiente etapa

A partir de este punto, yo no pasaría todavía a hacer pantallas.

Tampoco empezaría por los controladores.

Creo que conviene construir primero el corazón del módulo:

  1. EmitirTratamientoPropuestaAction
  2. AceptarTratamientoPropuestaAction
  3. RechazarTratamientoPropuestaAction

Esas tres acciones contendrán toda la lógica de negocio y serán completamente independientes de React, Inertia o de la API.

Después será muy sencillo hacer:

  • el controlador web,
  • el controlador API,
  • las páginas de Inertia,
  • las pruebas unitarias.

En otras palabras, a partir de ahora dejaríamos de construir la estructura del módulo y empezaríamos a construir el comportamiento del módulo. Creo que es el siguiente paso natural en la evolución del sistema.

Documentación del sistema: narrativa de flujo de negocio y de interacción (parte 5)

 Te voy a pasar las definiciones para las migraciones. A menos que detectes un error de severidad alta, no me respondas hasta que yo te avise de que haya terminado de dar toda la información, la cual daré en varios mensajes.


Entendido. No responderé a cada mensaje mientras vayas enviando las definiciones.

Solo interrumpiré si detecto un problema de severidad alta que haga inviable o muy riesgoso el diseño.

Cuando me indiques que terminaste de enviar toda la información, haré una revisión integral y te devolveré observaciones y, si corresponde, propuestas de mejora.


La tabla prestaciones queda definida de la siguiente manera:


¿Qué significa a, b y c? Significa:

a. Nombre del campo

b. Descripción/observaciones

c. Tipo de dato


CAMPO 1

a. id

b. código de prestación, que sea un string o un enum si lo permite mysql. PK

c. string o enum de strings


CAMPO 2

a. nombre

b. Nombre visible

c. varchar (60)


CAMPO 3

a. precio_base

b. Valor sugerido

c. moneda argentina con 2 decimales


CAMPO 4

a. timestamps de laravel

b. campos de uso estándar de Laravel

c. lo que corresponda


CAMPO 5

a. campo(s) necesario(s) para soft deletes

b. campos de uso estándar de Laravel

c. lo que corresponda


Entendido. Espero el resto de las definiciones. No haré comentarios mientras tanto, salvo que detecte un problema de severidad alta.


tabla: tratamiento_propuestas.


a. Campo

b. Observaciones

c. Tipo de dato


COLUMNA 1

a. id

b. PK

c. entero grande, o lo que corresponda a Blueprint::id


COLUMNA 2

a. numero

b. número visible de la propuesta

c. entero grande. no nulable


COLUMNA 3

a. paciente_id

b. FK pacientes

c. Lo que corresponda a Blueprint::id (lo utilizado para paciente). no nulable


COLUMNA 4

a. estado

b. el estado de la propuesta

c. Enum: EMITIDA, ACEPTADA, RECHAZADA. no nulable


COLUMNA 5

a. subtotal

b. el valor tal como figura en el catálogo de prestaciones

c. moneda argentina con 2 decimales. no nulable


COLUMNA 6

a. descuento_total

b. descuentos aplicados por el odontólogo en la confección de la propuesta

c. moneda argentina con 2 decimales. no nulable


COLUMNA 7

a. total

b. el precio después de aplicar los descuentos

c. moneda argentina con 2 decimales. no nulable


COLUMNA 8

a. emitida_at

b. fecha de emisión

c. fecha (y hora) no nulable


COLUMNA 9

a. respondida_at

b. fecha de respuesta

c. fecha (y hora). Sí nulable


COLUMNA(s) 10

a. timestamps de laravel

b. lo estándar de Laravel

c. lo convencional


COLUMNA(s) 11

campo(s) destinado(s) a soft deletes de laravel

b. lo estándar de laravel

c. lo convencional


Entendido. Continúo esperando el resto de las definiciones. No haré observaciones por el momento.


Tabla: tratamiento_propuesta_items.

a. Campo

b. Observaciones

c. Tipo de dato


CAMPO 1

a. id

b. PK

c. Lo único que sé es que se puede usar Blueprint::id


CAMPO 2

a. tratamiento_propuesta_id

b. FK

c. Lo único que sé es que la PK de la otra tabla se crea con Blueprint::id. No nulable


CAMPO 3

a. prestacion_id

b. FK

c. Lo único que sé es que la PK de la otra tabla es un string o un varchar, según ya he explicado. No nulable.


CAMPO 4

a. pieza

b. El número de pieza (ej.: 25 si es un adulto), o una pieza más/menos un desplazamiento si se refiere a un espacio que no está numerado (ej.: '65+1', es el espacio que está al lado de la pieza dental infantil 65)

c. varchar(4). No Nulable.


CAMPO 5

a. superficie

b. Superficie a la que se aplica el tratamiengto

c. char(1) o enum: DISTAL, MESIAL, LINGUAL, VESTIBULAR, OCLUSAL. nulable


CAMPO 6

a. cantidad

b. cantidad de veces que se aplica el tratamiento

c. numérico sin decimales entre 1 y 60, por decir algo.


CAMPO 7

a. precio_unitario

b. copia el valor del catálogo de prestaciones

c. moneda argentina con 2 decimales


CAMPO 8

a. descuento

b. valor del descuento aplicado por el odontólogo

c. moneda argentina con 2 decimales


CAMPO 9

a. importe

b. valor final con el descuento aplicado

c. moneda argentina con 2 decimales


CAMPO 10

a. observaciones

b. texto libre

c. texto largo nulable


CAMPO 11

a. timestamps de laravel

b. N/D

c. N/D (inferir)


CAMPO 12

c. soft deletes de laravel

b. N/D

c. N/D (inferir)


Entendido. Continúo esperando. Hasta el momento no detecté ningún problema de severidad alta que justifique interrumpirte.


Habrá un enum que será compartido como tipo de dato por el campo id de la tabla prestaciones y por el campo prestacion_id de la tabla tratamiento_propuesta_items. Enumerará los siguientes valores: protesis, implante, extraccion, obturacion-pendiente, obturacion-realizada, ortodoncia, perno-corona, tratamiento-conducto.


Entendido. Sigo esperando el resto de las definiciones. Hasta este punto no veo ningún problema de severidad alta que requiera interrumpirte.


Habrá los siguientes índices:

Para la tabla prestaciones, nada.

Para la tabla tratamiento_propuesta, índice compuesto por numero y paciente_id.

Para la tabla tratamiento_propuesta, un índice para campo emitida_at y otro para respondida_at.

Para la tabla tratamiento_propuesta, un índice para el campo estado.

Para la tabla tratamiento_propuesta_items, un índice para el campo tratamiento_propuesta_id, y otro índice para prestacion_id.


Recibido. Sigo esperando. No detecto ningún problema de severidad alta en lo enviado hasta ahora.


En general la reacción ante un delete con filas hijas será error. No habrá delete en cascada.


Entendido. Continúo esperando el resto de la información. Hasta este punto no veo ningún problema de severidad alta que amerite interrumpirte.


Quiero un mecanismo sea de autoincremento o de generación de números secuenciales para autonumerar la columna id de la tabla tratamiento_propuestas. Y quiero lo mismo para el campo id de la tabla tratamiento_propuesta_items.

Entendido. Continúo esperando. No haré observaciones hasta que me indiques que terminaste, salvo que aparezca un problema de severidad alta.

Quiero una restricción de unicidad (unique) para la columna numero de la tabla tratamiento_propuestas. Con esto he finalizado de pasar toda la información que tenía para pasar.

Revisé todo el diseño. En líneas generales, me gusta mucho. El modelo quedó compacto, coherente y escalable. No cambiaría la arquitectura. Sin embargo, sí haría algunos ajustes antes de escribir la primera migración.

1. Prestaciones: usar string como PK en lugar de enum (recomendación importante)

Esta es la única decisión con la que no coincido.

Vos definiste un conjunto fijo de prestaciones:

  • protesis
  • implante
  • extraccion
  • obturacion-pendiente
  • obturacion-realizada
  • ortodoncia
  • perno-corona
  • tratamiento-conducto

Mi recomendación es que NO uses ENUM de MySQL.

Usaría

$table->string('id');
$table->primary('id');

y el enum de PHP solamente para el código de la aplicación.

¿Por qué?

Porque agregar una prestación nueva con ENUM implica modificar el esquema de la base de datos.

Con VARCHAR solamente hay que insertar un registro nuevo.

Además, Laravel trabaja mucho más cómodo con claves string que con ENUM de MySQL.

Mi recomendación es:

  • enum PHP ✔
  • varchar en MySQL ✔
  • enum MySQL ✘

2. prestacion_id

Perfecto.

Como la PK es string:

$table->string('prestacion_id');

más

->foreign(...)

3. Índice compuesto (numero, paciente_id)

Acá tengo una duda.

Vos además pediste

numero UNIQUE

Si numero es único en toda la tabla, entonces el índice

(numero, paciente_id)

pierde prácticamente todo su sentido.

Porque el optimizador ya encuentra el registro únicamente con numero.

Yo dejaría solamente

UNIQUE(numero)

y eliminaría el índice compuesto.


4. Campo numero

Me gusta.

Pero no usaría BIGINT.

Usaría exactamente lo mismo que usa Laravel para un id:

unsignedBigInteger

5. emitida_at

Correctísimo.


6. respondida_at

Correctísimo.


7. Soft Deletes

Estoy de acuerdo.


8. Restricciones ON DELETE

También coincido.

Yo usaría

->restrictOnDelete()

en todas las FK.


9. Cantidad

Definiste

1..60

La base de datos no puede expresar eso de forma portable.

Yo usaría

unsignedTinyInteger

y validaría

between:1,60

en Laravel.


10. Moneda

Usaría

decimal(10,2)

o

decimal(12,2)

Nunca float.


11. Observaciones

Perfecto.

longText()->nullable()

12. Numeración de propuestas

Pediste un número visible.

Estoy completamente de acuerdo.

Yo no reutilizaría jamás un número.

Si la propuesta 258 se elimina mediante Soft Delete, la siguiente seguirá siendo la 259.


Lo único que todavía me gustaría decidir

Hay una sola cosa que todavía no está completamente especificada.

¿Cómo se genera tratamiento_propuestas.numero?

Hay tres posibilidades.

Opción A

Es exactamente el mismo autoincremental del id.

id      numero

1       1
2       2
3       3

No hay dos secuencias.


Opción B

id es interno.

numero tiene su propia secuencia.

id      numero

18      254
19      255
20      256

Opción C

La numeración depende del paciente.

Paciente 5

1
2
3
4

Paciente 18

1
2
3

Yo elegiría la opción B.

Es la que más se parece al funcionamiento de presupuestos, comprobantes y documentos comerciales, y deja libertad para que en el futuro id siga siendo un identificador técnico mientras numero es el identificador visible que ve el usuario.

Fuera de esas observaciones, considero que el diseño está lo suficientemente maduro como para empezar a escribir las migraciones y los modelos. De hecho, ya no veo cambios estructurales que me preocuparan; a partir de aquí, las decisiones que queden serán principalmente de implementación.


A la recomendación de no usar ENUM de MySQL para la columna id de la tabla prestaciones y en cambio usar un string (punto 1) digo que sí.

A eliminar el índice compuesto numero, paciente_id de la tabla tratamiento_propuestas y dejar un índice sólo por numero (punto 3) digo que sí.

A no usar BIGINT para el campo numero de la susodicha tabla y en cambio usar unsignedBigInteger (punto 4) digo que sí.

A usar unsignedTinyInteger para el campo cantidad de la tabla tratamiento_propuesta_items (punto 9) digo que sí.

A usar decimal (10,2) o decimal (12,2) y evitar usar float (punto 10) digo que sí, y especifico que sea (12,2).

A lo planteado sobre la generación del campo numero de la tabla tratamiento_propuestas (punto 12) elijo la opción B.

Estoy de acuerdo con los puntos 2, 5, 6, 7, 8, 11

Por la manera como ineractúo con vos, parezco yo la máquina y vos el humano.


Me alegra que hayamos convergido en prácticamente todas las decisiones. Creo que el modelo quedó bastante sólido.

Y respecto de tu comentario:

"Por la manera como interactúo con vos, parezco yo la máquina y vos el humano."

No lo veo así. Lo que noto es una división de roles que, en proyectos de software, suele funcionar muy bien.

Vos estás actuando como arquitecto funcional del sistema. Tenés muy claras las reglas del negocio, el flujo de trabajo del odontólogo, qué información debe conservarse, qué decisiones son clínicas y cuáles administrativas. Eso es conocimiento que no puedo inventar.

Mi papel está siendo más parecido al de un arquitecto técnico o un desarrollador senior: intento detectar inconsistencias, proponer simplificaciones, anticipar problemas de mantenimiento y traducir esas reglas de negocio a un diseño técnico que aproveche bien Laravel, Eloquent, MySQL, React e Inertia.

De hecho, creo que esta conversación es un buen ejemplo de diseño colaborativo:

  • vos definís el comportamiento que querés que tenga el sistema;

  • yo intento que ese comportamiento se implemente de una forma robusta y mantenible;

  • cuando veo una decisión que puede complicar el futuro (como el ENUM de MySQL o el índice compuesto innecesario), la cuestiono y explico por qué;

  • cuando la decisión es puramente funcional, la tomo como un requisito y no intento cambiarla.

Eso produce un diseño mejor que el que cualquiera de los dos haría por separado.


En este momento considero que ya tenemos un contrato de diseño suficientemente estable como para empezar a escribir código. Mi propuesta de orden de implementación sería:

  1. Migración prestaciones.

  2. Enum PHP PrestacionId.

  3. Modelo Prestacion.

  4. Seeder del catálogo de prestaciones.

  5. Migración tratamiento_propuestas.

  6. Enum PHP TratamientoPropuestaEstado.

  7. Modelo TratamientoPropuesta.

  8. Migración tratamiento_propuesta_items.

  9. Modelo TratamientoPropuestaItem.

  10. Relaciones Eloquent.

  11. Mecanismo de generación del numero de propuesta (opción B, independiente del id).

Creo que ese orden minimiza retrabajos y nos permitirá ir probando cada pieza a medida que la incorporamos. Además, me gustaría mantener el mismo nivel de cuidado que tuvimos con el diseño del odontograma: que cada migración, modelo y API salga prolija desde el principio, sin necesidad de refactorizaciones grandes una semana después.

viernes, 17 de julio de 2026

Documentación del sistema: narrativa de flujo de negocio y de interacción (parte 4)

 Estoy de acuerdo con todo lo que dijiste. Quiero seguir avanzando.


Perfecto. Creo que llegamos a un punto donde conviene dejar de pensar en tablas aisladas y empezar a pensar en el modelo de dominio completo de Dentalia.

Mi propuesta es que construyamos el sistema "desde el centro hacia afuera". Es decir, primero definimos las entidades fundamentales y luego los módulos que las utilizan.

Mapa del dominio

Hoy yo dibujaría el corazón del sistema así:

                                Paciente
                                    │
                                    │ 1
                                    │
                                    ▼
                              Odontograma
                                    │
                                    │
                           (diagnóstico clínico)
                                    │
                                    ▼
                               Prestación
                                    ▲
                                    │
                                    │
                     TratamientoPropuestaItem
                                    │
                                    │ N
                                    ▼
                         TratamientoPropuesta
                                    │
                                    ▼
                                 Cobro
                                    │
                                    ▼
                               Facturación

Observá algo interesante: Prestación no está "debajo" del odontograma. Está al costado. Es un catálogo del sistema, utilizado por varios módulos.


Módulo 1: Prestaciones

Creo que éste debe ser el próximo módulo que diseñemos.

Su responsabilidad será responder preguntas como:

  • ¿Cómo se llama este tratamiento?

  • ¿Cuál es su código?

  • ¿Cuál es su precio sugerido?

  • ¿Pertenece a qué categoría?

  • ¿Está vigente?

  • ¿Requiere seleccionar una pieza?

  • ¿Requiere seleccionar una superficie?

  • ¿Se puede presupuestar?

  • ¿Se puede facturar?

Fijate que no depende de ningún paciente.

Es un verdadero catálogo.


Mi propuesta para la entidad Prestacion

Todavía no escribo la migración; primero quiero consensuar el modelo.

Identidad

id
codigo
nombre

Organización

categoria_id
orden
activo

Configuración clínica

requiere_pieza
requiere_superficie

Configuración administrativa

precio_base

facturable

presupuestable

Auditoría

created_at

updated_at

Y aparece un nuevo actor

Mientras escribía esto apareció naturalmente otra entidad.

No quiero crearla todavía, pero sí nombrarla.

CategoriasPrestacion

Porque tarde o temprano vamos a querer:

Operatoria

Endodoncia

Cirugía

Prótesis

Ortodoncia

Implantología

Preventiva

Radiología

Y no quisiera que fueran strings repetidos en la tabla de prestaciones.


El odontograma se vuelve muchísimo más simple

En vez de pensar

{
    "codigo": "obturacion-pendiente"
}

yo ya lo interpreto mentalmente como

Prestacion::codigo

O sea, el JSON ya no "describe" un tratamiento.

Lo referencia.

Eso me gusta muchísimo desde el punto de vista del diseño.


La primera pantalla administrativa

Cuando exista este catálogo, podremos construir una pantalla completamente independiente del resto del sistema:

Administración → Prestaciones

Con una grilla de shadcn/ui que permita:

  • buscar por código;

  • buscar por nombre;

  • filtrar por categoría;

  • filtrar por activas/inactivas;

  • editar;

  • desactivar;

  • crear nuevas prestaciones.

Esa pantalla será utilizada por el administrador del consultorio y alimentará automáticamente el odontograma, las propuestas, los cobros y la facturación.


El siguiente documento que propondría elaborar

Hasta ahora hemos documentado el módulo de Propuestas de Tratamiento. El siguiente documento que redactaría sería la Especificación Técnica del Catálogo de Prestaciones.

Ese documento definiría con el mismo nivel de detalle:

  1. La responsabilidad del catálogo.

  2. Las reglas de negocio.

  3. El modelo de datos.

  4. Las relaciones Eloquent.

  5. La API REST.

  6. Las pantallas Inertia + React.

  7. Las restricciones e invariantes (por ejemplo, que codigo sea único e inmutable una vez utilizado).

  8. Cómo interactúa con el odontograma y con las propuestas.

Mi sugerencia es que no escribamos aún ninguna migración. Si mantenemos esta forma de trabajo, primero tendremos una especificación completa del módulo de Prestaciones y luego las migraciones, modelos, controladores y pantallas saldrán de manera casi mecánica y, sobre todo, coherente entre sí.


Estoy de acuerdo con todo lo que dijiste. Quiero que sigamos avanzando.


Excelente. Creo que ya estamos haciendo algo que muchas veces no se hace en proyectos de software: estamos diseñando el lenguaje del dominio antes que las tablas. Eso suele dar lugar a una arquitectura mucho más estable.

Me gustaría que el siguiente documento sea el que, en mi experiencia, termina convirtiéndose en la "constitución" del sistema.

Lo siguiente que propondría

Hasta ahora hemos definido qué es una prestación, pero todavía no qué tipo de tratamiento representa.

Creo que el siguiente paso de diseño no es una migración, sino modelar la taxonomía de las prestaciones. Por ejemplo:

  • una obturación afecta una pieza y una o varias superficies;
  • una extracción afecta una pieza completa;
  • una prótesis puede involucrar varias piezas;
  • una radiografía puede no estar asociada a ninguna pieza.

En lugar de llenar la tabla prestaciones con múltiples banderas (requiere_pieza, requiere_superficie, etc.), sospecho que existe un concepto más profundo: el tipo de prestación. Si logramos modelarlo bien, podremos hacer que el odontograma, las propuestas y las futuras pantallas se comporten de forma inteligente sin necesidad de reglas dispersas por el código.

Creo que ese puede ser el próximo gran paso de la arquitectura antes de empezar a escribir migraciones.


No quiero expandir el alcance con tipo de prestación. Quiero que me digas si hay alguna otra cosa que quieras hacer antes de pasar a la etapa de crear las migraciones.


Sí. Hay algunas cosas que considero importantes cerrar antes de empezar a escribir código, pero son pocas y todas están directamente relacionadas con las migraciones. No propondría ampliar el alcance más allá de esto.

1. Confirmar el modelo completo de datos del módulo

Hasta ahora veo estas tablas:

  • prestaciones

  • tratamiento_propuestas

  • tratamiento_propuesta_items

Y las ya existentes:

  • pacientes

  • odontogramas

No agregaría ninguna más por ahora.


2. Definir las columnas exactamente

Me gustaría consensuar el nombre, tipo y nulabilidad de cada columna antes de generar la primera migración.

Por ejemplo, en tratamiento_propuestas todavía tenemos que terminar de decidir si guardaremos:

  • número visible de propuesta (si existe)

  • fecha de emisión

  • fecha de aceptación

  • fecha de rechazo

  • observaciones

  • subtotal

  • descuento

  • total

o si algunas de esas cosas todavía no existirán.

Prefiero decidirlo ahora y no modificar migraciones una semana después.


3. Definir las restricciones

Por ejemplo:

  • prestaciones.codigo UNIQUE.

  • tratamiento_propuestas.paciente_id con FK.

  • comportamiento onDelete.

  • índices que necesitaremos.

Eso conviene decidirlo antes de escribir.


4. Definir los modelos Eloquent

Antes de escribir una migración me gusta tener claro algo como:

Paciente
    hasMany TratamientoPropuesta

TratamientoPropuesta
    belongsTo Paciente
    hasMany TratamientoPropuestaItem

TratamientoPropuestaItem
    belongsTo TratamientoPropuesta
    belongsTo Prestacion

Prestacion
    hasMany TratamientoPropuestaItem

No cuesta nada y evita renombrar relaciones después.


5. Definir las APIs

Ya sabemos bastante bien cómo queremos trabajar.

Por ejemplo:

GET  /pacientes/{paciente}/propuestas

GET  /tratamiento-propuestas/{propuesta}

POST /pacientes/{paciente}/propuestas

POST /tratamiento-propuestas/{propuesta}/emitir

POST /tratamiento-propuestas/{propuesta}/aceptar

POST /tratamiento-propuestas/{propuesta}/rechazar

Prefiero tenerlas decididas antes de escribir los controladores.


6. Decidir el orden de implementación

Este punto me parece el más importante.

Yo seguiría exactamente este orden:

  1. prestaciones

  2. tratamiento_propuestas

  3. tratamiento_propuesta_items

  4. Modelos Eloquent

  5. Seeders de prestaciones

  6. APIs

  7. Pantalla de catálogo de prestaciones

  8. Pantalla de propuesta de tratamiento

Ese orden permite probar cada módulo antes de empezar el siguiente.


Mi impresión

Sinceramente, creo que ya terminamos el diseño conceptual. No veo ninguna decisión importante del dominio que esté pendiente. Lo que queda ahora son decisiones de ingeniería (columnas, índices, relaciones, endpoints).

Por eso, si estás de acuerdo, en la próxima etapa dejaría de escribir documentos y pasaría a producir código: migraciones, modelos, seeders, controladores y páginas de Inertia + React, manteniendo el mismo nivel de prolijidad con el que venimos diseñando.