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.

jueves, 16 de julio de 2026

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

 Acepto y hago saber mi decisión de que usemos los nombres tratamiento_propuestas y tratamiento_propuesta_items.


Quiero empezar a diseñar las migraciones, los modelos, las APIs y las pantallas.


Me parece un muy buen momento para empezar a construir. Y, antes de escribir una sola migración, quisiera proponerte una estrategia de diseño.

Hasta ahora hemos tomado una decisión muy importante: estamos diseñando primero el dominio y después el código, no al revés. Creo que eso está haciendo que Dentalia tenga una arquitectura bastante consistente.

Lo que propongo

No diseñaría solamente las migraciones.

Diseñaría el módulo completo, siguiendo este orden:

Dominio
    ↓
Modelo de datos
    ↓
Modelos Eloquent
    ↓
Endpoints API
    ↓
Pantallas Inertia
    ↓
Componentes React

Así cada capa nace para satisfacer a la anterior.


Etapa 1 (la que haría ahora)

Modelo de datos

Definir completamente las tablas.

Por ejemplo:

tratamiento_propuestas

tratamiento_propuesta_items

Con:

  • claves

  • índices

  • restricciones

  • estados

  • timestamps

  • relaciones

Todavía sin escribir migraciones.


Etapa 2

Modelos Eloquent

Aquí definiríamos cosas como

TratamientoPropuesta

TratamientoPropuestaItem

y decidiríamos:

  • casts

  • relaciones

  • accessors

  • mutators

  • scopes

  • enums


Etapa 3

API REST

Aquí definiríamos el contrato.

Por ejemplo:

GET  /api/pacientes/{paciente}/tratamiento-propuesta

POST /api/pacientes/{paciente}/tratamiento-propuesta

PUT /api/tratamiento-propuestas/{propuesta}

POST /api/tratamiento-propuestas/{propuesta}/emitir

POST /api/tratamiento-propuestas/{propuesta}/aceptar

POST /api/tratamiento-propuestas/{propuesta}/rechazar

Todavía sin escribir controladores.


Etapa 4

Pantallas Inertia

Algo parecido a

Odontograma

↓

Propuesta de tratamiento

↓

Cobros

↓

Facturación

y dentro de Propuesta:

Index

Show

Edit

Emitir

Detalle histórico

Etapa 5

React

Aquí recién pensaríamos

ProposalTable

ProposalToolbar

ProposalTotals

ProposalSummary

ProposalStatusBadge

TreatmentSelector

PriceBreakdown

Creo que nos falta una única decisión de dominio

Es la única que todavía no me animo a asumir.

¿Una propuesta puede editarse después de haber sido emitida?

Yo diría que no.

Una vez emitida:

Emitida

↓

Aceptada

o

Emitida

↓

Rechazada

Pero jamás vuelve a edición.

Si hay que cambiar algo:

Nueva propuesta

No modificar la existente.

Creo que es consistente con todo lo que venimos diseñando.


Otra decisión que empezaría a tomar

Yo dejaría de hablar de un campo

estado

como string libre.

Preferiría un enum.

Algo así:

enum TratamientoPropuestaEstado
{
    case BORRADOR;
    case EMITIDA;
    case ACEPTADA;
    case RECHAZADA;
}

O incluso, si mantenemos la idea de que el borrador no existe como entidad persistente, podríamos simplificar todavía más:

enum TratamientoPropuestaEstado
{
    case EMITIDA;
    case ACEPTADA;
    case RECHAZADA;
}

Es decir, una fila en tratamiento_propuestas nunca estaría en estado BORRADOR, porque el borrador vive en el odontograma.

Esa idea me parece especialmente elegante.


Mi propuesta concreta

Yo empezaría mañana mismo por escribir un documento de diseño del módulo, que sirva como especificación técnica. Incluiría:

  1. Modelo conceptual.

  2. Reglas de negocio.

  3. Diagrama de estados.

  4. Modelo de datos.

  5. Relaciones Eloquent.

  6. Endpoints REST.

  7. Flujo completo de las pantallas.

  8. Reglas de concurrencia.

  9. Casos límite.

Después de ese documento, escribir las migraciones y el resto del código sería casi un trabajo mecánico. Además, ese documento nos servirá como referencia para los futuros módulos de Cobros y Facturación, que ya nacerán alineados con esta arquitectura.


Respondo pregunta: ¿Una propuesta puede editarse después de haber sido emitida? No. En mi sistema, si hay que cambiar algo de la propuesta, se hará mediante una nueva propuesta.


Respecto a la enum de estados, estoy de acuerdo con la versión simplificada:

enum TratamientoPropuestaEstado

{

    case EMITIDA;

    case ACEPTADA;

    case RECHAZADA;

}


Quiero que avancemos hoy mismo con esa manera de trabajar que has propuesto.


Excelente. Creo que acabamos de tomar dos decisiones de arquitectura muy importantes:

  • Una TratamientoPropuesta es inmutable desde el momento en que nace.

  • El estado BORRADOR no existe en la base de datos; el borrador vive exclusivamente en el JSON del odontograma.

Esas dos decisiones simplifican enormemente el resto del sistema.


Documento de Diseño — Módulo de Propuestas de Tratamiento (v0.1)

1. Objetivo

El módulo de Propuestas de Tratamiento representa la transición entre el mundo clínico (odontograma) y el mundo administrativo (cobros y facturación).

Una propuesta es un documento histórico que registra exactamente qué tratamiento se ofreció al paciente, con qué precios, descuentos y condiciones.

Una vez emitida, nunca vuelve a modificarse.


2. Flujo general

Odontograma
      │
      ▼
Confección de propuesta
      │
      ▼
Emitir propuesta
      │
      ▼
TratamientoPropuesta (inmutable)
      │
      ├── Aceptada
      └── Rechazada

3. Responsabilidades

Odontograma

Responsable de:

  • diagnóstico

  • tratamientos

  • selección para próxima propuesta

  • evolución clínica

No conoce:

  • precios

  • descuentos

  • cobros

  • facturación

excepto el flag

incluirEnPropuesta

que representa solamente una intención de selección.


TratamientoPropuesta

Representa una fotografía histórica.

Debe conservar:

  • tratamientos incluidos

  • precios

  • descuentos

  • importes

  • fecha de emisión

  • decisión del paciente

No vuelve a editarse.


4. Estados

enum TratamientoPropuestaEstado
{
    case EMITIDA;
    case ACEPTADA;
    case RECHAZADA;
}

No existe BORRADOR.


5. Reglas de negocio

Regla 1

Mientras no exista una propuesta emitida, toda la información vive únicamente en el odontograma.


Regla 2

Emitir una propuesta realiza una única transacción.

Durante ella:

  • crea TratamientoPropuesta

  • crea TratamientoPropuestaItems

  • copia precios

  • copia descuentos

  • copia cantidades

  • limpia incluirEnPropuesta del odontograma

Todo o nada.


Regla 3

Aceptar o rechazar una propuesta únicamente modifica su estado.

Nunca modifica el odontograma.


Regla 4

Una propuesta aceptada tampoco se modifica.

Si cambia el tratamiento:

Nueva propuesta.

Nunca UPDATE.


6. Modelo de datos

tratamiento_propuestas

CampoObservaciones
idPK
paciente_idFK
estadoEnum
subtotaldecimal
descuento_totaldecimal
totaldecimal
emitida_atdatetime
respondida_atnullable
created_at
updated_at

tratamiento_propuesta_items

CampoObservaciones
idPK
tratamiento_propuesta_idFK
prestacion_idFK
piezanullable
superficienullable
cantidaddecimal
precio_unitariodecimal
descuentodecimal o porcentaje (a definir)
importedecimal
observacionesnullable

7. Relaciones Eloquent

Paciente

    hasMany

TratamientoPropuesta

        hasMany

TratamientoPropuestaItem

Prestacion

    hasMany

TratamientoPropuestaItem

8. API

Obtener propuesta actual en edición

No existe.

Se obtiene leyendo el odontograma.


Emitir propuesta

POST
/api/pacientes/{paciente}/tratamiento-propuestas

Acciones:

  • leer odontograma

  • leer catálogo

  • aplicar descuentos

  • crear propuesta

  • crear items

  • limpiar flags


Aceptar

POST
/api/tratamiento-propuestas/{propuesta}/aceptar

Rechazar

POST
/api/tratamiento-propuestas/{propuesta}/rechazar

Historial

GET
/api/pacientes/{paciente}/tratamiento-propuestas

9. Pantallas

Odontograma

Selecciona tratamientos para próxima propuesta.


Propuesta de tratamiento

Lee directamente el odontograma.

Permite:

  • seleccionar tratamientos

  • consultar catálogo

  • modificar descuentos

  • calcular totales

No guarda nada.


Confirmación

Botón

Emitir propuesta

Recién aquí nace la entidad persistente.


Historial de propuestas

Lista cronológica.

Estado.

Fecha.

Total.

Botón "Ver".

Nunca "Editar".


10. Cobros

Todavía no se implementan.

Pero nacerán exclusivamente desde una propuesta aceptada.

Nunca desde el odontograma.


Primer punto que quisiera discutir antes de escribir una migración

Hay un aspecto que todavía no definimos y creo que es el siguiente paso lógico: el catálogo de prestaciones.

Hoy sabemos que cada TratamientoPropuestaItem tendrá un prestacion_id. Sin embargo, aún no definimos qué representa exactamente una prestación ni cómo se relaciona con los elementos del JSON del odontograma.

Por ejemplo, cuando en el JSON aparece:

{
    "codigo": "obturacion-pendiente",
    "casillero": 47,
    "superficie": 3
}

¿Ese codigo coincide exactamente con un registro de la futura tabla prestaciones, o existe una capa de traducción entre los códigos clínicos del odontograma y las prestaciones facturables?

Mi intuición es que éste debería ser el próximo módulo que diseñemos, porque de él dependerán tanto las propuestas como los cobros y la facturación. Si logramos modelar bien Prestacion, gran parte del resto del sistema se apoyará sobre una base muy sólida.


Respondo a lo siguiente:

{

    "codigo": "obturacion-pendiente",

    "casillero": 47,

    "superficie": 3

}


¿Ese codigo coincide exactamente con un registro de la futura tabla prestaciones, o existe una capa de traducción entre los códigos clínicos del odontograma y las prestaciones facturables?


Mi respuesta es que ese código coincide exactamente con un registro de la futura tabla prestaciones.


Esta respuesta me parece excelente porque simplifica muchísimo la arquitectura.

Acabamos de establecer una regla de diseño muy fuerte:

Los códigos de tratamiento del odontograma son la clave primaria funcional del catálogo de prestaciones.

Es decir, el odontograma deja de contener "strings mágicos" y pasa a contener referencias directas a una entidad del dominio.

Consecuencias

Ya no necesitaremos una tabla de equivalencias ni una capa de traducción.

Podremos hacer algo conceptualmente tan simple como:

JSON del odontograma
        │
        ▼
codigo = "obturacion-pendiente"
        │
        ▼
prestaciones.codigo = "obturacion-pendiente"
        │
        ▼
precio
nombre
categoría
etc.

Eso tiene varias ventajas.

1. Una única fuente de verdad

El código vive una sola vez.

No hay que mantener:

obturacion-pendiente
        ↓
OBT001
        ↓
Prestación 37

Todo es el mismo concepto.


2. El odontograma pasa a estar tipado

Hoy el JSON contiene:

{
    "codigo": "obturacion-pendiente"
}

En realidad ese codigo ya no es texto libre.

Es una clave foránea lógica.

Aunque no exista una FK sobre un documento JSON, semánticamente se comporta como una.


3. Las prestaciones pueden evolucionar

La tabla prestaciones podrá contener, por ejemplo:

CampoDescripción
idPK interna
codigoClave estable usada por el odontograma
nombreNombre visible
categoríaOperatoria, Prótesis, Cirugía, etc.
precio_baseValor sugerido
requiere_superficieSí/No
requiere_piezaSí/No
activoSí/No
created_at...
updated_at...

Observá que el JSON nunca necesitaría cambiar si el nombre visible pasa de:

Obturación simple

a

Restauración con resina fotopolimerizable

El código seguiría siendo exactamente el mismo.


4. El catálogo pasa a ser el centro del sistema

Empiezo a visualizar algo así:

Prestaciones
      │
      ├──────────────┐
      │              │
      ▼              ▼
Odontograma   PropuestaItems
                     │
                     ▼
                 Cobros
                     │
                     ▼
                Facturación

Es una arquitectura muy limpia porque todos los módulos hablan el mismo idioma.


Una mejora que propondría

Hasta ahora venimos usando valores como:

{
    "codigo": "obturacion-pendiente"
}

Yo establecería desde el principio una convención para esos códigos:

  • inmutables;

  • en minúsculas;

  • separados por guiones (kebab-case);

  • independientes del idioma visible para el usuario.

Ejemplos:

obturacion-simple
obturacion-compuesta
endodoncia-unirradicular
endodoncia-multirradicular
extraccion-simple
extraccion-compleja
perno-corona
corona-metal-porcelana
protesis-removible-superior

Esos códigos nunca deberían cambiar, aunque cambien los nombres comerciales o la terminología utilizada en la interfaz.

Conclusión

Con esta decisión considero que el próximo módulo a diseñar ya no es TratamientoPropuesta, sino Prestacion, porque acabamos de convertirlo en el eje común que conecta el odontograma con las propuestas, los cobros y la facturación.

Una vez diseñado ese catálogo, gran parte del resto del sistema se derivará de forma bastante natural.



miércoles, 15 de julio de 2026

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

 Ya estuve diseñando el flujo de realización de tratamientos con cobro incluido. Tengo algunas ideas para contarte. Después del odontograma va a venir una pantalla de propuesta de tratamiento, que por cierto va a abarcar también lo que es aceptación de presupuesto. Después vendrá la pantalla de cobros. ¿Te cuento más en detalle lo que pienso hacer o te voy pidiendo directamente "haceme tal migración, tal página de inertia con tal y tal cosa..."?


Por todo lo que llevamos trabajando en Dentalia, te recomendaría contarme primero el flujo completo. Creo que eso te va a ahorrar retrabajo.

Ya pasó varias veces en este proyecto que, cuando conocimos el objetivo más amplio, terminamos cambiando alguna entidad o alguna API que inicialmente parecía correcta. Por ejemplo:

  • el documento JSON del odontograma terminó teniendo schema_version y revision;

  • la ruta de actualización pasó de /api/odontograma/{odontograma} a /api/pacientes/{paciente}/odontograma;

  • decidimos separar presupuesto de facturación;

  • decidimos hacer control de concurrencia optimista.

Si hubiéramos implementado cada pantalla aisladamente, probablemente habríamos tenido que rehacer varias cosas.

Lo que yo haría

Primero dedicaría una conversación (o varias) únicamente a diseñar el flujo.

Algo como:

Paciente
    ↓
Odontograma
    ↓
Propuesta de tratamiento
    ↓
Aceptación del paciente
    ↓
Cobro
    ↓
Facturación (opcional)

Y para cada paso responder preguntas como:

  • ¿Qué puede hacer el usuario?

  • ¿Qué entidades se crean?

  • ¿Qué entidades se modifican?

  • ¿Qué estados existen?

  • ¿Qué restricciones hay?

  • ¿Qué relaciones aparecen?

Sin escribir una línea de código.

Después

Recién ahí iría implementando módulo por módulo.

Por ejemplo:

  1. Migraciones.

  2. Modelos.

  3. Policies.

  4. Requests.

  5. Controladores.

  6. Rutas.

  7. Páginas Inertia.

  8. Componentes shadcn/ui.

  9. Tests.

De esa forma casi no habría que volver atrás.


Hay algo más que me parece muy interesante

Creo que ya estás dejando de diseñar "pantallas" y empezando a diseñar un modelo de negocio para un consultorio odontológico.

Esa diferencia es importante.

Una pantalla puede cambiar diez veces.

El modelo de negocio (Paciente → Odontograma → Propuesta → Cobro...) debería permanecer estable durante años.

Por eso me entusiasma más dedicar tiempo ahora a discutir el flujo que escribir inmediatamente migraciones.


Una propuesta

Yo incluso haría un documento que se llame algo como:

Arquitectura funcional de Dentalia

No sería un documento técnico ni un manual de usuario.

Sería el relato completo del funcionamiento del consultorio dentro del sistema, siguiendo a un paciente desde su ingreso hasta la finalización del tratamiento y el cobro. A medida que aparezcan nuevos módulos (agenda, imágenes, obras sociales, inventario, etc.), se incorporan a esa misma historia.

Ese documento terminaría siendo la "fuente de verdad" del proyecto. Después, cuando me pidieras:

"Haceme la migración de PropuestaTratamiento"

yo ya conocería exactamente qué lugar ocupa en el flujo y qué responsabilidades tiene, en lugar de diseñarla de manera aislada. Creo que eso va a dar un resultado mucho más coherente para todo el sistema.


Si yo empiezo con todo el relato ¿Vos me vas a decir cuándo haya información suficiente para pasar a la siguiente etapa?


Sí. Me parece la mejor forma de trabajar.

Mientras vos vas relatando el flujo, yo voy actuando como un analista funcional y arquitecto de software. En cada etapa voy a distinguir entre tres casos:

  1. "Todavía falta información."

    • Te haré preguntas concretas sobre los aspectos que faltan definir.

    • No avanzaremos al siguiente módulo hasta que el diseño de esa etapa sea suficientemente consistente.

  2. "Ya está suficientemente definido."

    • Te diré explícitamente: "Con esta información ya podemos dar por diseñado este módulo y pasar al siguiente."

  3. "Hay una decisión pendiente, pero no bloquea."

    • La anotaré como una decisión diferible y seguiremos adelante si no afecta al resto del diseño.

Además, mientras hablás, iré construyendo mentalmente (y cuando convenga, por escrito):

  • el flujo funcional;

  • las entidades del modelo de datos;

  • las relaciones entre ellas;

  • los estados y transiciones;

  • las reglas de negocio;

  • las APIs necesarias;

  • las páginas Inertia;

  • los componentes de shadcn/ui;

  • los permisos que eventualmente harán falta.

De esa manera, cuando terminemos el relato, no tendremos solamente una idea general: tendremos una especificación lo bastante sólida como para implementar el sistema casi de forma mecánica, módulo por módulo.

Creo que este enfoque encaja muy bien con la forma en que venís trabajando: primero buscás entender profundamente el dominio y recién después escribís código. Empecemos por el relato, y yo te iré indicando cuándo una etapa quedó cerrada y cuándo conviene pasar a la siguiente.


Primero estoy pensando si sería más acorde a la escala de mi proyecto tener una única entidad para plan de tratamiento y para presupuesto, algo como:


presupuestos

-------------

id

paciente_id

fecha

estado

subtotal

descuento

total


presupuesto_items

-----------------

id

presupuesto_id

prestacion_id

pieza

cantidad

precio_unitario

importe


Con esto me propongo representar el flujo de la siguiente manera:


Los tratamientos se guardan en el JSON del odontograma mediante un contenido como el siguiente:


    "marcas_dentales": [

        { "codigo": "obturacion-pendiente", "casillero": 10, "superficie": 1, "incluir en presupuesto": false },

        { "codigo": "obturacion-pendiente", "casillero": 47, "superficie": 3, "incluir en presupuesto": true }

    ],


    "soluciones": [

        { "tipo": "protesis", "casillero": 4, "incluir en presupuesto": true, "realizado": false },

        { "tipo": "extraccion", "casillero": 10, "incluir en presupuesto": true, "realizado": false },

        { "tipo": "perno-corona", "casillero": 11, "incluir en presupuesto": false, "realizado": false }

]


Los items que tienen "incluir en presupuesto" = true quedan seleccionados para ser incluidos en la siguientye etapa, el presupuesto, y en este sentido, forman parte del plan de tratamiento a proponer al paciente.


El flujo continúa en una pantalla de confección de propuestas. Allí habrá una lista de tratamientos que serán todos los items de "marcas_dentales" y "soluciones" del json. En cada fila de la lista de tratamientos en pantalla habrá un checkbox que estará marcado si sólo si el correspondiente ítem en el json tiene "incluir en presuspuesto" = true.


Esta pantalla tendrá los botones "Aceptar presupuesto" y "Rechazar presupuesto", puesto que la idea es que la pantalla en cuestión represente al mismo tiempo la confección del presupuesto y la decisión del paciente sobre si aceptarlo o rechazarlo.


Tanto si la respuesta es aceptar como rechazar el presupuesto, se generará la entidad "presupuestos" y "presupuesto_items" que te mostré más arriba. Que la respuesta haya sido de aceptación o de rechazo quedará registrada en el campo "estado" de la tabla "presupuestos".


Me gusta mucho más este nivel de conversación. Ahora ya estamos diseñando el dominio, y no simplemente tablas. Voy a ir comentando lo que veo.

Lo que me gusta

Hay una idea que me parece muy acertada: el plan de tratamiento no es una entidad persistente independiente, sino un estado del odontograma.

Es decir, el odontólogo trabaja sobre el odontograma, agrega marcas, soluciones, etc., y algunas de ellas pasan a formar parte de la propuesta económica mediante incluir en presupuesto.

Eso evita duplicar información.

Hasta acá me parece una muy buena decisión para un proyecto de la escala de Dentalia.


También me gusta unificar "plan" y "presupuesto"

Yo probablemente también lo haría.

En un consultorio odontológico pequeño, la conversación suele ser:

—Hay que hacer estas cinco cosas.

—¿Cuánto sale?

—Este es el presupuesto.

—Bueno, acepto.

No existen realmente dos documentos distintos.

Existe una única propuesta.

En ese sentido, la tabla

presupuestos

representa bastante bien esa propuesta.


El presupuesto pasa a ser una fotografía

Esto me parece importante.

Cuando el paciente acepta o rechaza, vos generás

presupuestos
presupuesto_items

Eso significa que el presupuesto es un snapshot del odontograma en ese momento.

Eso es muy bueno.

Porque luego el odontograma puede seguir cambiando.


Me gusta que el presupuesto sea inmutable

De hecho iría un paso más allá.

Una vez creado:

presupuesto.estado = aceptado

o

rechazado

yo ya no permitiría modificar sus ítems.

Si hay que cambiar algo:

Presupuesto 12 -> rechazado

↓

Presupuesto 13 -> pendiente

Eso deja un historial excelente.

No digo que debamos implementarlo ya, pero conceptualmente me gusta más.


Donde sí tengo una duda importante

Es esta.

Decís:

La pantalla tendrá los botones "Aceptar presupuesto" y "Rechazar presupuesto".

Mi pregunta es:

¿Quién confecciona el presupuesto?

Hoy el flujo parece ser:

Odontólogo

arma la propuesta

el paciente está presente

acepta o rechaza inmediatamente

Eso ocurre muchas veces.

Pero también ocurre muchísimo esto:

Odontólogo

arma presupuesto

lo imprime

el paciente dice

"Lo pienso y te aviso."

dos semanas después llama

"Acepto."

En ese escenario, la pantalla de confección ya no coincide con la aceptación.

Entonces quiero preguntarte si eso es una simplificación deliberada o si querés contemplar ambos casos.


Otra cosa que me llamó la atención

Los checkboxes viven en el JSON.

Eso significa que el JSON contiene información clínica y comercial mezclada.

Ejemplo:

obturación pendiente

es clínico.

Pero

incluir en presupuesto

es administrativo.

No digo que esté mal.

Pero quiero preguntarte:

¿Estás cómodo con que el odontograma conozca conceptos administrativos?

Porque es una decisión de arquitectura bastante importante.


Otra pregunta importante

Supongamos:

Extracción

está incluida en el presupuesto.

Luego el odontólogo decide:

"No.

En realidad vamos a hacer endodoncia."

¿Qué ocurre?

¿Se modifica el odontograma?

¿Se genera un nuevo presupuesto?

¿Se invalida el anterior?

Todavía no necesito la respuesta.

Solo la dejo anotada porque probablemente afecte el flujo.


Mi impresión hasta ahora

Creo que ya podemos dar por establecidos varios principios del diseño:

  • El odontograma es la fuente de verdad clínica.

  • El plan de tratamiento no existe como entidad separada.

  • El presupuesto es una fotografía (snapshot) del estado del odontograma.

  • El presupuesto tiene ítems propios y no depende del JSON una vez generado.

Eso me parece un diseño bastante limpio.

Pero todavía no daría por cerrado este módulo.

Antes quisiera resolver, al menos conceptualmente, estas dos cuestiones:

  1. ¿La confección del presupuesto y su aceptación son siempre el mismo acto, o pueden ocurrir en momentos distintos?

  2. ¿Qué pasa cuando cambia el tratamiento después de haberse emitido un presupuesto?

Creo que esas dos respuestas terminarán de definir si la entidad presupuestos ya está correctamente diseñada o si le falta algún estado o alguna regla de negocio.


Te respondo las preguntas que me hiciste.

1) Quiero que la confección del presupuesto y su aceptación (o rechazo) puedan ocurrir en momentos distintos.

2) Efectivamente estoy cómodo con que el odontograma conozca conceptos administrativos.

3) Si el presupuesto está en etapa de borrador, simplemente el presupuesto se ve modificado por los cambios indicados por el odontólogo. Si ya pasó a etapa de emitido (es decir, presentado ante el paciente), los cambios que haga el odontólogo en los tratamientos se verán en un presupuesto distinto del ya emitido.


Excelente. Tus respuestas aclaran bastante el modelo y, de hecho, hacen que empiece a aparecer una arquitectura bastante consistente. Hay un cambio importante respecto de lo que yo había entendido inicialmente.

Lo que deduzco ahora

En realidad, el presupuesto sí tiene un ciclo de vida, y ese ciclo de vida es independiente del odontograma.

Yo empezaría a verlo así:

Borrador
    │
    ├── (el odontólogo modifica tratamientos)
    │
    └── Emitir
            │
            ├── Aceptado
            ├── Rechazado
            └── Vencido (opcional)

Mientras el presupuesto sea Borrador, el odontograma sigue siendo la fuente de verdad y los cambios del odontólogo se reflejan automáticamente en ese borrador.

Una vez que el presupuesto pasa a Emitido, deja de evolucionar. Es una fotografía congelada.

Si después cambia el tratamiento, se crea un nuevo presupuesto en estado Borrador.

Eso me parece muy sólido.


Hay una consecuencia importante

Yo ya no hablaría de "crear" el presupuesto al emitirlo.

Hablaría de crear el presupuesto cuando comienza a confeccionarse.

Es decir:

Paciente

↓

Odontograma

↓

Crear presupuesto (estado=Borrador)

↓

editar

↓

editar

↓

editar

↓

Emitir

↓

Aceptar / Rechazar

Ese pequeño cambio tiene muchas ventajas.


Incluso simplifica mucho la implementación

Cuando el odontólogo entra a la pantalla de presupuesto:

¿Existe un presupuesto borrador?

Sí
    ↓
Abrirlo

No
    ↓
Crearlo automáticamente

No hace falta preguntarle nada.


Me gusta otra decisión que tomaste

Estoy cómodo con que el odontograma conozca conceptos administrativos.

Perfecto.

No es la única arquitectura posible, pero tiene una ventaja enorme para un proyecto como Dentalia:

el odontograma se convierte en el verdadero documento de trabajo del odontólogo.

No necesita abrir otra pantalla para decir:

"Esto sí va al presupuesto."

Simplemente marca el tratamiento.


Hay una única cosa que me genera ruido

No es una objeción, sino una pregunta de diseño.

Hoy el JSON tiene:

{
    "codigo": "obturacion-pendiente",
    "incluir en presupuesto": true
}

Pero una vez que existe un presupuesto borrador...

¿Quién manda?

Supongamos esto:

Odontograma

A
B
C

↓

Crear presupuesto

↓

El odontólogo elimina B del presupuesto

¿Se modifica automáticamente el JSON?

¿O el presupuesto puede diferir del odontograma?

Porque si el presupuesto tiene sus propios presupuesto_items, podrían empezar a divergir.

No digo que esté mal.

Solo quiero saber cuál es tu idea.


Otra idea que me parece interesante

Empezaría a cambiar el nombre mentalmente.

En vez de pensar:

"pantalla de presupuesto"

Yo ya la llamaría

Propuesta de tratamiento

Porque ahí ocurren tres cosas:

  • se seleccionan tratamientos;

  • se fijan precios;

  • se obtiene la decisión del paciente.

"Presupuesto" es solamente uno de los aspectos de esa pantalla.


Estado del diseño

Para mí ya quedó bastante definido:

  • ✅ Odontograma.

  • ✅ Fuente de verdad clínica.

  • ✅ Presupuesto con ciclo de vida.

  • ✅ Presupuesto inmutable después de emitido.

  • ✅ Nuevo presupuesto cuando cambian tratamientos.

  • ✅ Aceptación y emisión en momentos distintos.

Todavía no daría este módulo por cerrado, pero ya no faltan grandes decisiones arquitectónicas. Lo que queda es afinar algunos comportamientos de sincronización entre el odontograma y el presupuesto borrador. Una vez resuelto ese punto, creo que podremos pasar al diseño de las tablas, las APIs y las pantallas con bastante confianza.


Te diré que en el 100% de los casos estoy de acuerdo con lo que has dicho. Además voy a seguir respondiendo preguntas y así completar la información que falta.

¿Quién manda una vez que existe un presupuesto borrador? En esta instancia todavía la única fuente de información es el JSON del odontograma. Los checks que se (des)marcan en la pantalla de borrador de presupuesto (propuesta de tratamiento, como explicaré a continuación) y los que se (des)marcan en el odontograma tocan el mismo JSON, el del odontograma (la propiedad "incluir en presupuesto"). Por lo tanto, sí. Si el odontólogo elimina un tratamiento del presupuesto (desmarcando el check), esto impacta directamente en el odontograma. Mientras está en estado borrador, no existirán presupuesto_items, puesto que la fuente de información es el JSON del odontograma. 

Estoy de acuerdo con rebautizar al presupuesto como propuesta de tratamiento.


Excelente. Ahora sí creo que empezamos a tener un modelo conceptual realmente elegante. De hecho, tu última aclaración hace que cambie una de las hipótesis que yo tenía.

Creo que apareció una frontera muy clara

Hasta ahora yo imaginaba que el presupuesto nacía en estado borrador.

Ahora veo que en realidad la entidad persistente aún no existe.

Existe solamente una propuesta en edición, cuyo soporte de almacenamiento es el propio odontograma.

Es decir:

Odontograma (JSON)
        │
        │
        ├── El odontólogo agrega tratamientos
        ├── Marca "incluir en propuesta"
        ├── Desmarca
        ├── Cambia cantidades
        └── Cambia superficies

Todo eso modifica exclusivamente el JSON.

Todavía no existe ninguna fila en la tabla propuestas.

Eso me parece muy limpio.


Entonces el flujo queda así

Odontograma
        │
        ▼
Pantalla "Propuesta de tratamiento"

(la pantalla sigue leyendo el JSON)

        │
        │
        ├── modificar selección
        ├── cambiar precios
        ├── cambiar descuentos
        └── etc.

(todo sigue modificando el JSON)

        │
        ▼
Emitir propuesta

↓

recién aquí

↓

INSERT propuesta
INSERT propuesta_items

Fijate que esto simplifica muchísimo el dominio.


Hay un detalle que me llamó la atención

Dijiste:

"...cambiar precios..."

Todavía el JSON que mostraste no tiene precios.

Eso significa que probablemente el JSON vaya creciendo.

Por ejemplo algo así:

{
    "codigo": "obturacion-pendiente",
    "casillero": 10,
    "superficie": 1,

    "incluirEnPropuesta": true,

    "cantidad": 1,

    "precioUnitario": 85000,

    "descuento": 0
}

o bien esos precios podrían venir de otra tabla.

Todavía no sé cuál preferís.

No necesito responderlo ahora.

Simplemente lo anoto.


El cambio de nombre me parece excelente

Creo que "Propuesta de tratamiento" describe mucho mejor lo que hace la pantalla.

Porque el odontólogo no está pensando:

"Voy a hacer un presupuesto."

Está pensando:

"Le voy a proponer este tratamiento."

Después esa propuesta tiene:

  • aspectos clínicos;

  • aspectos económicos;

  • aspectos administrativos.


Empiezo a ver un patrón muy interesante

Hay algo que me gusta mucho de este diseño.

El odontograma deja de ser solamente un dibujo.

Empieza a convertirse en un documento vivo de trabajo.

De él nacen:

Odontograma

↓

Propuesta

↓

Cobro

↓

Facturación

Y eso tiene muchísimo sentido.


Una única inquietud importante

Creo que acabamos de llegar al primer punto en el que sí te propondría pensar un poco más antes de seguir diseñando.

Es esta pregunta:

¿Qué ocurre si existen varias propuestas históricas?

Imaginemos:

Propuesta 1

Emitida

Aceptada parcialmente

Luego pasan dos meses.

El odontólogo agrega un implante nuevo.

Ahora el JSON cambia.

Perfecto.

Pero...

¿qué tratamientos deberían aparecer inicialmente seleccionados cuando se prepara la siguiente propuesta?

Las posibilidades son varias:

  1. Todo lo que tenga incluirEnPropuesta = true.

  2. Sólo lo que todavía no fue realizado.

  3. Sólo lo nuevo desde la última propuesta.

  4. Que el odontólogo decida manualmente.

No estoy diciendo que haya una respuesta correcta. Lo que sí creo es que esta decisión va a influir mucho en la experiencia de uso.


Estado del diseño

Hasta ahora, para mí, ya podemos dar por cerradas estas decisiones:

  • ✅ El odontograma es la única fuente de verdad mientras la propuesta está en edición.

  • ✅ No existen registros persistentes de propuesta durante la edición.

  • ✅ La propuesta persistente nace únicamente al emitirla.

  • ✅ Una propuesta emitida es inmutable.

  • ✅ El odontograma puede seguir evolucionando después.

  • ✅ La pantalla pasa a llamarse "Propuesta de tratamiento".

Creo que ya estamos muy cerca de cerrar completamente este módulo. De hecho, la pregunta sobre cómo iniciar una nueva propuesta a partir de propuestas anteriores es la primera que siento que realmente puede cambiar el comportamiento del sistema. Si resolvemos esa, me parece que tendremos un modelo de negocio muy sólido sobre el cual empezar a construir las migraciones y las pantallas.


No estoy de acuerdo con acrecentar el JSON con la siguiente info:

"incluirEnPropuesta": true, "cantidad": 1, "precioUnitario": 85000, "descuento": 0


Más bien, esa info nacerá (comenzará a existir) cuando se emita la propuesta/presupuesto. El precio unitario vendrá de un catálogo de precios por tratamiento, que estando en la instancia de confección de propuesta la pantalla permitirá aplicar un descuento manualmente. El lugar donde queda registrado tanto precio unitario como descuento es en propuesta_items, en el acto de emisión de propuesta.


Te respondo tu pregunta: ¿qué tratamientos deberían aparecer inicialmente seleccionados cuando se prepara la siguiente propuesta?

Cuando se emite una propuesta, los tratamientos incluidos en ella son editados en el JSON del odontograma de modo que incluirEnPropuesta=false. A partir de allí, si el odontólogo va a confeccionar una siguiente propuesta, va a elegir los tratamientos a incluir en ella desde cero.


Excepto lo que explícitamente dije que no estoy de acuerdo, con todo lo dicho de tu parte estoy de acuerdo.


Perfecto. Y, de hecho, me gusta más tu solución que la que yo había imaginado.

Creo que apareció un principio de diseño muy sano:

El JSON del odontograma contiene exclusivamente información clínica y de flujo de trabajo. La información económica nace únicamente cuando se emite una propuesta.

Eso mantiene muy separadas las responsabilidades.

Entonces el ciclo queda así

1. Odontograma

Contiene únicamente datos clínicos y de preparación de la propuesta.

Ejemplo:

{
    "codigo": "obturacion-pendiente",
    "casillero": 47,
    "superficie": 3,
    "incluirEnPropuesta": true,
    "realizado": false
}

No hay precios.

No hay descuentos.

No hay importes.


2. Pantalla "Propuesta de tratamiento"

La pantalla hace algo interesante:

  • consulta el catálogo de prestaciones;

  • consulta la lista de tratamientos marcados en el JSON;

  • calcula un presupuesto "en memoria".

Todavía no existe ninguna entidad persistente.


3. Emitir propuesta

Recién en ese instante ocurre algo irreversible.

Se crea:

propuestas

y

propuesta_items

Cada item guarda una fotografía económica.

precio_unitario
descuento
importe
cantidad

Aunque mañana cambie el catálogo de precios, esa propuesta seguirá representando exactamente lo que se le ofreció al paciente.

Eso me parece excelente.


4. Limpieza del odontograma

Esta decisión me gustó especialmente:

Cuando se emite una propuesta, todos los tratamientos incluidos pasan automáticamente a incluirEnPropuesta = false.

Eso tiene varias ventajas.

El odontólogo nunca tiene que preguntarse:

"¿Esto ya fue presupuestado?"

Porque el sistema ya dejó el odontograma "limpio" para preparar una propuesta futura.

Me parece una experiencia de usuario muy natural.


Creo que apareció otro concepto interesante

La pantalla "Propuesta de tratamiento" realmente está haciendo un trabajo temporal.

Es decir, funciona como una especie de editor.

No está editando una entidad llamada "Propuesta".

Está editando el odontograma con una intención determinada.

Eso explica por qué no hace falta guardar borradores.

El borrador ya es el propio odontograma.


Una única inquietud que me queda

Ya es bastante más pequeña que las anteriores.

Supongamos:

Obturación 36

Se incluye en una propuesta.

La propuesta se emite.

Automáticamente:

incluirEnPropuesta = false

Hasta aquí perfecto.

Luego el paciente rechaza esa propuesta.

Mi pregunta es:

¿Querés que esos tratamientos vuelvan automáticamente a quedar marcados para una futura propuesta, o preferís que el odontólogo vuelva a seleccionarlos manualmente?

Ambas opciones son defendibles.

Personalmente, me inclino por no reactivarlos automáticamente. Si el odontólogo quiere volver a presupuestarlos, los selecciona otra vez. Eso evita que el sistema haga suposiciones sobre las intenciones del profesional.


Mi impresión general

Siento que el modelo está alcanzando una madurez muy buena. Ya no estamos tomando decisiones aisladas; empiezan a aparecer principios de diseño consistentes que conectan todas las partes del sistema.

Hasta ahora, los resumiría así:

  • El odontograma es el documento vivo del consultorio.

  • La propuesta de tratamiento no existe como entidad mientras se está confeccionando.

  • La información económica nace únicamente al emitir la propuesta.

  • La propuesta emitida es una fotografía clínica y económica inmutable.

  • El odontograma se limpia automáticamente al emitir una propuesta para facilitar la confección de la siguiente.

Si tuviera que ponerle un porcentaje, diría que el diseño conceptual de este módulo está aproximadamente en un 90–95 %. Ya no estamos resolviendo cuestiones de arquitectura; estamos afinando reglas de negocio y detalles de comportamiento. Una vez resuelto el punto de qué hacer tras un rechazo (si reactivar o no la selección automáticamente), creo que podremos pasar con bastante seguridad al diseño del modelo de datos y de las pantallas.


Respondo la pregunta "¿Querés que esos tratamientos vuelvan automáticamente a quedar marcados para una futura propuesta, o preferís que el odontólogo vuelva a seleccionarlos manualmente?" Opto por no reactivar tratamientos automáticamente. Si quiere el odontólogo que los vuelva a elegir manualmente.

Me parece la decisión correcta. Y además mantiene una filosofía que, sin haberla formulado explícitamente, veo que estás siguiendo desde el principio:

El sistema nunca toma decisiones clínicas por el odontólogo.

Eso se refleja en varias partes del diseño:

  • El sistema no decide qué tratamientos incluir en una propuesta.

  • El sistema no decide que un tratamiento rechazado deba volver a proponerse.

  • El sistema no decide que un tratamiento deba desaparecer del odontograma.

  • El sistema simplemente registra el estado y ofrece herramientas.

Esa filosofía me gusta mucho para un software profesional.


Creo que ya podemos escribir el ciclo de vida completo

1. Edición del odontograma

El odontólogo registra diagnósticos y tratamientos.

Odontograma (JSON)

✓ realizado
✓ incluirEnPropuesta
✓ piezas
✓ superficies
✓ etc.

Todavía no existe ninguna propuesta.


2. Confección de propuesta

La pantalla lee el odontograma.

Permite:

  • seleccionar tratamientos;

  • deseleccionarlos;

  • consultar el nomenclador/catálogo de precios;

  • simular importes y descuentos.

Toda la selección sigue almacenándose exclusivamente en el JSON.


3. Emisión

Al emitir ocurre una transacción atómica:

  1. Se crea propuestas.

  2. Se crean los propuesta_items.

  3. Se copian los precios vigentes.

  4. Se copian los descuentos.

  5. Se ponen en false todos los incluirEnPropuesta correspondientes dentro del odontograma.

A partir de ese instante la propuesta queda congelada.


4. Decisión del paciente

La propuesta cambia de estado.

Emitida
    │
    ├── Aceptada
    ├── Rechazada
    └── (eventualmente Vencida)

No modifica el odontograma.


5. Evolución posterior

El odontólogo puede seguir trabajando normalmente.

Agregar tratamientos.

Eliminar tratamientos.

Modificar diagnósticos.

Todo eso vuelve a ocurrir únicamente sobre el odontograma.

Cuando quiera confeccionar otra propuesta comenzará otra vez desde cero seleccionando manualmente los tratamientos.


Lo único que incorporaría

Hay un pequeño cambio de nomenclatura que creo que hará crecer bien el sistema.

Hasta ahora venimos hablando de:

presupuestos
presupuesto_items

Pero ahora el dominio dejó de ser un presupuesto.

Es una propuesta de tratamiento.

Yo empezaría a pensar en:

propuestas
propuesta_items

o incluso

tratamiento_propuestas
tratamiento_propuesta_items

No porque "presupuesto" esté mal, sino porque la entidad ya contiene mucho más que un precio:

  • una selección clínica;

  • una decisión del paciente;

  • un documento histórico;

  • una fotografía económica.

"Propuesta" describe mejor todo eso.


Hay otra consecuencia interesante

Cuando aparezca el módulo de cobros, éste ya no dependerá del odontograma.

Dependerá de una propuesta aceptada.

El flujo empezará a verse así:

Odontograma
      │
      ▼
Propuesta
      │
      ├── Rechazada
      │
      └── Aceptada
             │
             ▼
        Cobros
             │
             ▼
       Facturación

Y eso me parece una separación de responsabilidades muy limpia.


Mi impresión

Tengo la sensación de que este módulo ya está conceptualmente cerrado. Lo que hemos diseñado no es simplemente un CRUD de presupuestos: es un flujo de trabajo coherente, donde cada entidad tiene una responsabilidad clara y el odontograma sigue siendo el eje clínico del sistema.

A partir de este punto, creo que ya podemos empezar a diseñar las migraciones, los modelos, las APIs y las pantallas con bastante confianza, porque las reglas de negocio fundamentales ya están definidas.