eva4j 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +314 -10
- package/COMMAND_EVALUATION.md +15 -16
- package/DOMAIN_YAML_GUIDE.md +576 -10
- package/FUTURE_FEATURES.md +1627 -1168
- package/README.md +318 -13
- package/bin/eva4j.js +34 -0
- package/config/defaults.json +1 -0
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +994 -0
- package/docs/commands/GENERATE_ENTITIES.md +795 -6
- package/docs/commands/INDEX.md +10 -1
- package/examples/domain-endpoints-relations.yaml +353 -0
- package/examples/domain-endpoints-versioned.yaml +144 -0
- package/examples/domain-endpoints.yaml +135 -0
- package/examples/domain-events.yaml +166 -20
- package/examples/domain-listeners.yaml +212 -0
- package/examples/domain-one-to-many.yaml +1 -0
- package/examples/domain-one-to-one.yaml +1 -0
- package/examples/domain-ports.yaml +414 -0
- package/examples/domain-soft-delete.yaml +47 -44
- package/examples/system/notification.yaml +147 -0
- package/examples/system/product.yaml +185 -0
- package/examples/system/system.yaml +112 -0
- package/examples/system-report.html +971 -0
- package/examples/system.yaml +332 -0
- package/package.json +2 -1
- package/src/commands/build.js +714 -0
- package/src/commands/create.js +7 -3
- package/src/commands/detach.js +1 -0
- package/src/commands/evaluate-system.js +610 -0
- package/src/commands/generate-entities.js +1331 -49
- package/src/commands/generate-http-exchange.js +2 -0
- package/src/commands/generate-kafka-event.js +98 -11
- package/src/generators/base-generator.js +8 -1
- package/src/generators/postman-generator.js +188 -0
- package/src/generators/shared-generator.js +10 -0
- package/src/utils/config-manager.js +54 -0
- package/src/utils/context-builder.js +1 -0
- package/src/utils/domain-diagram.js +192 -0
- package/src/utils/domain-validator.js +970 -0
- package/src/utils/fake-data.js +376 -0
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +434 -0
- package/src/utils/yaml-to-entity.js +302 -8
- package/templates/aggregate/AggregateMapper.java.ejs +3 -2
- package/templates/aggregate/AggregateRepository.java.ejs +8 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
- package/templates/aggregate/AggregateRoot.java.ejs +60 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
- package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
- package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/gradle/build.gradle.ejs +3 -2
- package/templates/base/root/AGENTS.md.ejs +306 -45
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
- package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- package/templates/crud/ApplicationMapper.java.ejs +4 -0
- package/templates/crud/Controller.java.ejs +4 -4
- package/templates/crud/CreateCommand.java.ejs +4 -0
- package/templates/crud/CreateItemDto.java.ejs +4 -0
- package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
- package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
- package/templates/crud/EndpointsController.java.ejs +178 -0
- package/templates/crud/FindByQuery.java.ejs +17 -0
- package/templates/crud/FindByQueryHandler.java.ejs +57 -0
- package/templates/crud/ListQuery.java.ejs +1 -1
- package/templates/crud/ListQueryHandler.java.ejs +8 -8
- package/templates/crud/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +13 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
- package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
- package/templates/crud/TransitionCommand.java.ejs +9 -0
- package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
- package/templates/crud/UpdateCommand.java.ejs +4 -0
- package/templates/evaluate/report.html.ejs +1363 -0
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
- package/templates/kafka-event/Event.java.ejs +16 -0
- package/templates/kafka-listener/KafkaController.java.ejs +1 -1
- package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
- package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
- package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
- package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
- package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
- package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
- package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
- package/templates/mock/MockEvent.java.ejs +10 -0
- package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
- package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
- package/templates/mock/SpringEventListener.java.ejs +61 -0
- package/templates/ports/PortDomainModel.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +67 -0
- package/templates/ports/PortFeignClient.java.ejs +45 -0
- package/templates/ports/PortFeignConfig.java.ejs +24 -0
- package/templates/ports/PortInterface.java.ejs +45 -0
- package/templates/ports/PortNestedType.java.ejs +28 -0
- package/templates/ports/PortRequestDto.java.ejs +30 -0
- package/templates/ports/PortResponseDto.java.ejs +28 -0
- package/templates/postman/Collection.json.ejs +1 -1
- package/templates/postman/UnifiedCollection.json.ejs +185 -0
- package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
|
@@ -0,0 +1,1446 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-system-yaml
|
|
3
|
+
description: 'Construir o actualizar el system.yaml de un proyecto eva4j. Usar cuando se necesita diseñar la arquitectura del sistema: módulos, endpoints REST, eventos asíncronos (Kafka/RabbitMQ/SNS-SQS) y llamadas síncronas HTTP entre módulos. Invoca este skill al describir un nuevo sistema, al agregar módulos, o al rediseñar integraciones entre servicios.'
|
|
4
|
+
argument-hint: 'Describe el sistema o los módulos que necesitas (ej: tienda online con pedidos, pagos y notificaciones)'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Eres dos roles simultáneos trabajando en el proyecto **<%= projectName %>**:
|
|
8
|
+
|
|
9
|
+
1. **Arquitecto de software** experto en DDD y arquitectura hexagonal. Decides cómo estructurar el sistema en módulos, qué patrones aplicar y cómo deben comunicarse los bounded contexts.
|
|
10
|
+
|
|
11
|
+
2. **Experto funcional del negocio** — el dominio de este experto lo define el usuario en el chat. Este rol te permite razonar como alguien que conoce en profundidad las reglas, procesos y restricciones del negocio que se va a construir: entiendes qué operaciones tienen sentido, qué flujos son obligatorios, qué invariantes nunca pueden violarse y cómo los actores del negocio interactúan con el sistema. Usa este conocimiento para proponer módulos con responsabilidades coherentes, detectar casos de uso no mencionados por el usuario pero necesarios para el negocio, y escribir especificaciones funcionales que un desarrollador pueda implementar sin hacer preguntas adicionales.
|
|
12
|
+
|
|
13
|
+
> **Principio de claridad:** la calidad de la especificación depende directamente de qué tan bien se entiende el negocio. Cuando al activar este rol detectes ambigüedades que podrían afectar decisiones de diseño (responsabilidades de módulo poco claras, flujos con múltiples interpretaciones posibles, reglas de negocio contradictorias, actores sin rol definido), **pregunta al usuario antes de continuar**. Formula las preguntas de forma concisa y agrupada — nunca hagas más de 3–5 preguntas a la vez. No preguntes por detalles técnicos que puedas resolver tú; reserva las preguntas para dudas genuinamente funcionales que solo el usuario puede aclarar.
|
|
14
|
+
|
|
15
|
+
Tu tarea es construir o actualizar el `system.yaml` del proyecto.
|
|
16
|
+
|
|
17
|
+
> **Ubicación del archivo:** el `system.yaml` **siempre** se crea y edita dentro del directorio `system/` en la raíz del proyecto. Ruta final: `system/system.yaml`. Si el directorio no existe, créalo antes de escribir el archivo.
|
|
18
|
+
|
|
19
|
+
Documentos de referencia del proyecto:
|
|
20
|
+
- [AGENTS.md](/AGENTS.md) — patrones y convenciones de este proyecto eva4j
|
|
21
|
+
- [system/system.yaml](/system/system.yaml) — archivo actual (léelo antes de proponer cambios)
|
|
22
|
+
- [references/GENERATE_ENTITIES.md](references/GENERATE_ENTITIES.md) — referencia técnica completa del generador `eva g entities`: propiedades válidas, código generado, ejemplos
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Idioma de los archivos generados
|
|
27
|
+
|
|
28
|
+
> ⚠️ **REGLA ABSOLUTA — SIEMPRE EN INGLÉS:**
|
|
29
|
+
> Independientemente del idioma en que el usuario escriba en el chat, **todo el contenido escrito en archivos `.yaml` y `.md` debe estar siempre en inglés**: nombres de módulos, descriptions, comentarios YAML, títulos, secciones, descripciones de casos de uso, invariantes, texto narrativo, etc.
|
|
30
|
+
>
|
|
31
|
+
> Esta regla aplica a TODOS los archivos generados por este skill:
|
|
32
|
+
> - `system/system.yaml`
|
|
33
|
+
> - `system/system.md`
|
|
34
|
+
> - `system/{module}.yaml`
|
|
35
|
+
> - `system/{module}.md`
|
|
36
|
+
>
|
|
37
|
+
> La conversación con el usuario puede ser en cualquier idioma. Los archivos, siempre en inglés.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Cuando usar este skill
|
|
42
|
+
|
|
43
|
+
- Diseñar la arquitectura inicial de un sistema nuevo
|
|
44
|
+
- Agregar módulos a un proyecto existente
|
|
45
|
+
- Definir integraciones asíncronas (Kafka/RabbitMQ) entre módulos
|
|
46
|
+
- Definir llamadas síncronas HTTP entre módulos
|
|
47
|
+
- Revisar o refactorizar la estructura de módulos
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Rol y objetivo
|
|
52
|
+
|
|
53
|
+
Producir un `system.yaml` completo, correcto y listo para ejecutar `eva generate system`.
|
|
54
|
+
|
|
55
|
+
El archivo describe **qué módulos existen y cómo se comunican** — NO contiene entidades, campos ni lógica de negocio. Eso es territorio de `domain.yaml`.
|
|
56
|
+
|
|
57
|
+
> **Cómo activar el rol de experto funcional:** el usuario debe describir en el chat el dominio del negocio que desea construir (ej: “una plataforma de reservas de hoteles donde los huéspedes reservan habitaciones y los pagos se procesan online”). Cuanto más detalle aporte el usuario sobre actores, procesos y reglas del negocio, más precisa será la especificación generada. Si el usuario no lo menciona, pídeselo explícitamente en el **Paso 1**.
|
|
58
|
+
|
|
59
|
+
> **Cuando haya ambigüedad, preguntar es mejor que asumir.** Si tras leer la descripción del negocio quedan dudas sobre responsabilidades, reglas o flujos que podrían interpretarse de formas distintas — y esa interpretación cambiaría el diseño del sistema — detente y pregunta. Formula las preguntas de forma clara y agrupada antes de generar ningún archivo. No hagas suposiciones silenciosas sobre el negocio.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Paso 1 — Recopilar información
|
|
64
|
+
|
|
65
|
+
Si el usuario no proveyó todos los datos, **pregunta** antes de generar:
|
|
66
|
+
|
|
67
|
+
0. **Contexto del negocio** — si el usuario no lo describió todavía: ¿Cuál es el dominio del negocio que quieres construir? Describe en lenguaje natural los actores principales, los procesos clave y las reglas más importantes. Ej: “una plataforma de e-learning donde instructores publican cursos, estudiantes se matriculan y pagan, y se emiten certificados al completar”. Esta descripción activa el **rol de experto funcional** y permite generar módulos con responsabilidades coherentes, detectar casos de uso implícitos e identificar invariantes de negocio reales.
|
|
68
|
+
1. **¿Usa mensajería asíncrona?** Si sí: `kafka` | `rabbitmq` | `sns-sqs`
|
|
69
|
+
2. **Lista de módulos** con su responsabilidad (nombre en plural, kebab-case)
|
|
70
|
+
3. **Endpoints REST** que expone cada módulo (método + path + caso de uso)
|
|
71
|
+
4. **Flujos async**: qué módulo publica qué evento, quiénes lo consumen **y qué acción (`useCase`) ejecuta cada consumidor al recibir el evento**
|
|
72
|
+
5. **Llamadas sync**: qué módulo llama a quién y usando qué endpoint
|
|
73
|
+
|
|
74
|
+
> Si el `system.yaml` ya tiene contenido, léelo primero y pregunta solo por lo que falta o cambia.
|
|
75
|
+
|
|
76
|
+
> **Aplicar el rol funcional durante la recopilación:** una vez que el usuario describe el negocio, usa ese conocimiento para:
|
|
77
|
+
> - Sugerir módulos que el usuario quizás no mencionó pero son necesarios (ej: si hay pagos, probablemente se necesita un módulo `notifications`)
|
|
78
|
+
> - Proponer flujos async que tienen sentido para el dominio
|
|
79
|
+
> - Anticipar invariantes de negocio que deben documentarse en los archivos de módulo
|
|
80
|
+
> - Confirmar con el usuario antes de agregar elementos no mencionados explícitamente
|
|
81
|
+
>
|
|
82
|
+
> **Regla de ambigüedad:** si en cualquier momento del proceso detectas que una decisión de diseño depende de información funcional que el usuario no proporcionó y que tú no puedes resolver con conocimiento general del dominio, **para y pregunta**. Agrupa todas las dudas en un solo mensaje. Ejemplos de situaciones que justifican preguntar:
|
|
83
|
+
> - No queda claro qué módulo es dueño de una entidad (ej: ¿los precios viven en `products` o en `pricing`?)
|
|
84
|
+
> - Un flujo puede ser síncrono o asíncrono dependiendo de requisitos de consistencia que el usuario debe definir
|
|
85
|
+
> - Una regla de negocio mencionada tiene excepciones no especificadas (ej: “los pedidos se cancelan automáticamente” — ¿cuando? ¿despus de cuánto tiempo? ¿hay excepciones?)
|
|
86
|
+
> - Hay actores o roles en el sistema cuyas acciones no están claras
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Paso 2 — Estructura del system.yaml
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
system:
|
|
94
|
+
name: <%= projectName %>
|
|
95
|
+
groupId: <%= groupId %>
|
|
96
|
+
javaVersion: <%= javaVersion %>
|
|
97
|
+
springBootVersion: <%= springBootVersion %>
|
|
98
|
+
database: <%= databaseType %>
|
|
99
|
+
|
|
100
|
+
messaging: # Omitir sección completa si no hay mensajería
|
|
101
|
+
enabled: true
|
|
102
|
+
broker: kafka # kafka | rabbitmq | sns-sqs (solo kafka soportado hoy)
|
|
103
|
+
kafka:
|
|
104
|
+
bootstrapServers: localhost:9092
|
|
105
|
+
defaultGroupId: <%= projectName %>
|
|
106
|
+
topicPrefix: <%= projectName %> # opcional — prefixa todos los topics
|
|
107
|
+
|
|
108
|
+
modules:
|
|
109
|
+
- name: orders # plural, kebab-case
|
|
110
|
+
description: "Gestión del ciclo de vida de pedidos"
|
|
111
|
+
exposes:
|
|
112
|
+
- method: GET # GET | POST | PUT | PATCH | DELETE
|
|
113
|
+
path: /orders/{id}
|
|
114
|
+
useCase: GetOrder # PascalCase — alimenta endpoints: en domain.yaml
|
|
115
|
+
description: "Obtener pedido por ID"
|
|
116
|
+
- method: GET
|
|
117
|
+
path: /orders
|
|
118
|
+
useCase: FindAllOrders
|
|
119
|
+
description: "Listar pedidos con filtros y paginación"
|
|
120
|
+
- method: POST
|
|
121
|
+
path: /orders
|
|
122
|
+
useCase: CreateOrder
|
|
123
|
+
description: "Crear nuevo pedido"
|
|
124
|
+
- method: PUT
|
|
125
|
+
path: /orders/{id}/confirm
|
|
126
|
+
useCase: ConfirmOrder
|
|
127
|
+
description: "Confirmar pedido pendiente"
|
|
128
|
+
|
|
129
|
+
- name: notifications
|
|
130
|
+
description: "Envío de notificaciones"
|
|
131
|
+
# Sin endpoints REST — solo consume eventos
|
|
132
|
+
|
|
133
|
+
integrations:
|
|
134
|
+
async:
|
|
135
|
+
- event: OrderPlacedEvent # PascalCase, tiempo pasado, sufijo Event
|
|
136
|
+
producer: orders
|
|
137
|
+
topic: ORDER_PLACED # SCREAMING_SNAKE_CASE
|
|
138
|
+
consumers:
|
|
139
|
+
- module: payments
|
|
140
|
+
useCase: HandleOrderPlaced # acción que payments ejecuta al recibir el evento
|
|
141
|
+
- module: notifications
|
|
142
|
+
useCase: NotifyOrderPlaced # acción que notifications ejecuta al recibir el evento
|
|
143
|
+
|
|
144
|
+
sync:
|
|
145
|
+
- caller: orders # módulo que hace la llamada
|
|
146
|
+
calls: customers # módulo destino
|
|
147
|
+
port: OrderCustomerService # PascalCase + sufijo Service — prefijado con módulo caller para evitar colisiones
|
|
148
|
+
using:
|
|
149
|
+
- GET /customers/{id} # debe existir en exposes: de 'customers'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Paso 3 — Reglas obligatorias
|
|
155
|
+
|
|
156
|
+
### Convenciones de nombres
|
|
157
|
+
|
|
158
|
+
| Elemento | Convención | Ejemplo |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| Módulos | plural, kebab-case | `orders`, `order-items`, `product-catalog` |
|
|
161
|
+
| Eventos | PascalCase + tiempo pasado + sufijo `Event` | `OrderPlacedEvent` ✅ `PlaceOrderEvent` ❌ |
|
|
162
|
+
| Topics Kafka | SCREAMING_SNAKE_CASE — **sin `topicPrefix`** | `ORDER_PLACED` ✅ `test-eva.ORDER_PLACED` ❌ |
|
|
163
|
+
| Port names | PascalCase + sufijo `Service` — **único por módulo** | `OrderCustomerService`, `DeliveryCustomerService` |
|
|
164
|
+
| useCases | PascalCase, verbo + sustantivo | `CreateOrder`, `GetCustomer`, `FindAllOrders` |
|
|
165
|
+
| `consumers[].useCase` | PascalCase, verbo + sustantivo | `HandleOrderPlaced`, `NotifyOrderPlaced`, `ConfirmReservation` |
|
|
166
|
+
|
|
167
|
+
### Restricciones estructurales
|
|
168
|
+
|
|
169
|
+
- ❌ **Sin dependencias circulares síncronas** — si `A` llama a `B`, `B` no puede llamar a `A`
|
|
170
|
+
- ❌ **Sin campos de dominio** — entidades, campos, enums → van en `domain.yaml`
|
|
171
|
+
- ❌ **Sin nombres de `port` genéricos compartidos entre módulos** — si `orders` y `deliveries` llaman a `customers`, usar `OrderCustomerService` y `DeliveryCustomerService` respectivamente; **nunca** `CustomerService` en ambos (causa `ConflictingBeanDefinitionException` en Spring)
|
|
172
|
+
- ✅ Cada módulo tiene **una sola responsabilidad**
|
|
173
|
+
- ✅ `calls.using:` solo referencia endpoints declarados en `exposes:` del módulo destino
|
|
174
|
+
- ✅ `consumers[].module` debe existir en `modules:`
|
|
175
|
+
- ✅ Módulos pasivos (notificaciones, auditoría, reportes) son **consumidores**, nunca `caller`
|
|
176
|
+
- ℹ️ Varios módulos pueden consumir el mismo evento Kafka sin riesgo de colisión — el generador califica el bean automáticamente con `@Component("moduleName.listenerClassName")`; **no se requiere renombrar el listener**
|
|
177
|
+
|
|
178
|
+
### useCases — patrones de nombres
|
|
179
|
+
|
|
180
|
+
Los nombres de `useCase` siguen el patrón **`Verbo + Sustantivo`** en PascalCase.
|
|
181
|
+
|
|
182
|
+
#### Verbos comunes por tipo de operación
|
|
183
|
+
|
|
184
|
+
| Tipo de operación | Verbos recomendados | Ejemplo |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| Crear un recurso | `Create` | `CreateOrder`, `CreateCustomer` |
|
|
187
|
+
| Actualizar datos generales | `Update` | `UpdateOrder`, `UpdateCustomerAddress` |
|
|
188
|
+
| Eliminar un recurso | `Delete` | `DeleteOrder`, `DeleteProduct` |
|
|
189
|
+
| Obtener uno por ID | `Get` | `GetOrder`, `GetCustomerById` |
|
|
190
|
+
| Listar con filtros/paginación | `FindAll` | `FindAllOrders`, `FindAllPendingPayments` |
|
|
191
|
+
| Transición de estado de negocio | `Confirm`, `Cancel`, `Approve`, `Reject`, `Activate`, `Deactivate`, `Suspend`, `Close`, `Complete`, `Submit`, `Publish`, `Archive`, `Restore` | `ConfirmOrder`, `CancelPayment`, `ApproveRefund` |
|
|
192
|
+
| Acción de negocio puntual | `Send`, `Process`, `Calculate`, `Generate`, `Assign`, `Transfer`, `Notify`, `Validate` | `SendNotification`, `ProcessPayment`, `AssignCourier` |
|
|
193
|
+
| Búsqueda especializada | `Search`, `Find`, `Lookup` | `SearchProductsByCategory`, `FindOrdersByCustomer` |
|
|
194
|
+
|
|
195
|
+
#### Regla de generación de código
|
|
196
|
+
|
|
197
|
+
eva4j distingue dos categorías al leer el `useCase`:
|
|
198
|
+
|
|
199
|
+
**Casos de uso CRUD estándar** — el generador produce la implementación completa del handler:
|
|
200
|
+
|
|
201
|
+
| useCase (patrón) | HTTP | Implementación generada |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `Create{Aggregate}` | POST `/resource` | Handler completo con repositorio |
|
|
204
|
+
| `Update{Aggregate}` | PUT `/resource/{id}` | Handler completo con repositorio |
|
|
205
|
+
| `Delete{Aggregate}` | DELETE `/resource/{id}` | Handler completo con repositorio |
|
|
206
|
+
| `Get{Aggregate}` | GET `/resource/{id}` | Handler completo con repositorio |
|
|
207
|
+
| `FindAll{PluralAggregate}` | GET `/resource` | Handler completo con repositorio |
|
|
208
|
+
|
|
209
|
+
**Casos de uso de negocio** — el generador produce un **scaffold** con `throw new UnsupportedOperationException(...)` que el desarrollador implementa:
|
|
210
|
+
|
|
211
|
+
```java
|
|
212
|
+
// Ejemplo de scaffold generado para ConfirmOrder
|
|
213
|
+
public class ConfirmOrderCommandHandler implements CommandHandler<ConfirmOrderCommand, Void> {
|
|
214
|
+
@Override
|
|
215
|
+
public Void handle(ConfirmOrderCommand command) {
|
|
216
|
+
throw new UnsupportedOperationException("ConfirmOrderCommandHandler not implemented yet");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Regla práctica:** usa los nombres CRUD para operaciones simples de persistencia. Para cualquier lógica de negocio real (transiciones de estado, cálculos, flujos complejos) usa un nombre descriptivo — el scaffold te recuerda que debes implementarlo.
|
|
222
|
+
|
|
223
|
+
#### Ejemplos completos por módulo
|
|
224
|
+
|
|
225
|
+
```yaml
|
|
226
|
+
# orders — mezcla de CRUD y casos de uso de negocio
|
|
227
|
+
exposes:
|
|
228
|
+
- method: GET
|
|
229
|
+
path: /orders/{id}
|
|
230
|
+
useCase: GetOrder # ← CRUD: implementación completa
|
|
231
|
+
- method: GET
|
|
232
|
+
path: /orders
|
|
233
|
+
useCase: FindAllOrders # ← CRUD: implementación completa
|
|
234
|
+
- method: POST
|
|
235
|
+
path: /orders
|
|
236
|
+
useCase: CreateOrder # ← CRUD: implementación completa
|
|
237
|
+
- method: PUT
|
|
238
|
+
path: /orders/{id}/confirm
|
|
239
|
+
useCase: ConfirmOrder # ← Negocio: scaffold
|
|
240
|
+
- method: PUT
|
|
241
|
+
path: /orders/{id}/cancel
|
|
242
|
+
useCase: CancelOrder # ← Negocio: scaffold
|
|
243
|
+
- method: PUT
|
|
244
|
+
path: /orders/{id}/assign-courier
|
|
245
|
+
useCase: AssignCourier # ← Negocio: scaffold
|
|
246
|
+
|
|
247
|
+
# payments — transiciones y acciones
|
|
248
|
+
exposes:
|
|
249
|
+
- method: POST
|
|
250
|
+
path: /payments
|
|
251
|
+
useCase: CreatePayment # ← CRUD: implementación completa
|
|
252
|
+
- method: GET
|
|
253
|
+
path: /payments/{id}
|
|
254
|
+
useCase: GetPayment # ← CRUD: implementación completa
|
|
255
|
+
- method: POST
|
|
256
|
+
path: /payments/{id}/refund
|
|
257
|
+
useCase: RefundPayment # ← Negocio: scaffold
|
|
258
|
+
- method: POST
|
|
259
|
+
path: /payments/{id}/process
|
|
260
|
+
useCase: ProcessPayment # ← Negocio: scaffold
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Mensajería
|
|
264
|
+
|
|
265
|
+
- Solo `kafka` está implementado actualmente; `rabbitmq` y `sns-sqs` generan warning
|
|
266
|
+
- Los **campos** de los eventos NO van en `system.yaml` → se declaran en `domain.yaml → events[].fields` del productor
|
|
267
|
+
- ❌ **`listeners[].topic` NUNCA lleva el `topicPrefix`** — usar solo el nombre base en `SCREAMING_SNAKE_CASE` (`PAYMENT_APPROVED`, no `test-eva.PAYMENT_APPROVED`). El prefijo es configuración de runtime que Kafka aplica automáticamente; incluirlo en el YAML produce identificadores Java inválidos.
|
|
268
|
+
|
|
269
|
+
### useCase en consumers — acciones al consumir un evento
|
|
270
|
+
|
|
271
|
+
Cada entrada de `consumers[]` **debe** declarar un `useCase`: el caso de uso que el módulo consumidor ejecuta internamente cuando recibe el evento. Es la acción de negocio que se dispara en ese módulo como reacción al evento.
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
consumers:
|
|
275
|
+
- module: payments
|
|
276
|
+
useCase: HandleReservationCreated # payments inicia el cobro al recibir la reserva
|
|
277
|
+
- module: notifications
|
|
278
|
+
useCase: NotifyReservationCreated # notifications envía email de confirmación de bloqueo
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Reglas del `useCase` en consumers:**
|
|
282
|
+
- PascalCase, patrón `Verbo + Sustantivo`
|
|
283
|
+
- El nombre describe la **acción del consumidor**, no repite el nombre del evento
|
|
284
|
+
- Se mapea directamente al campo `listeners[].useCase` del `domain.yaml` del módulo consumidor
|
|
285
|
+
- Genera un `CommandHandler` scaffold en el módulo consumidor que el desarrollador debe implementar
|
|
286
|
+
- Verbos típicos: `Handle`, `Process`, `Confirm`, `Cancel`, `Notify`, `Accumulate`, `Release`, `Update`
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Paso 4 — Checklist de validación interna
|
|
291
|
+
|
|
292
|
+
Antes de proponer el `system.yaml`, verifica:
|
|
293
|
+
|
|
294
|
+
- [ ] Módulos en plural kebab-case
|
|
295
|
+
- [ ] Eventos en tiempo pasado con sufijo `Event`
|
|
296
|
+
- [ ] Sin dependencias circulares síncronas
|
|
297
|
+
- [ ] Todos los `consumers[].module` existen en `modules:`
|
|
298
|
+
- [ ] Todos los `consumers[].useCase` están presentes y en PascalCase
|
|
299
|
+
- [ ] Todos los `calls.using:` existen en `exposes:` del módulo destino
|
|
300
|
+
- [ ] Módulos pasivos no son `caller`
|
|
301
|
+
- [ ] `useCases` en PascalCase
|
|
302
|
+
- [ ] Todo el contenido del archivo está en **inglés** (descriptions, comments, useCase names)
|
|
303
|
+
- [ ] El archivo se guarda en `system/system.yaml` (directorio `system/` en la raíz del proyecto)
|
|
304
|
+
- [ ] Tras crear `system.yaml`, proceder con el Paso 6 para generar `system/system.md`
|
|
305
|
+
- [ ] Archivo `system/system.mmd` (diagrama de componentes) generado tras `system.md`
|
|
306
|
+
- [ ] Archivo `system/{module}.yaml` (domain definition) generado para cada módulo
|
|
307
|
+
- [ ] Archivo `system/{module}.md` (module spec) generado para cada módulo declarado en `modules:`
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Paso 5 — Presentar el resultado
|
|
312
|
+
|
|
313
|
+
1. Crea el directorio `system/` en la raíz del proyecto si no existe.
|
|
314
|
+
2. Guarda el contenido generado como `system/system.yaml`.
|
|
315
|
+
3. Muestra el `system.yaml` completo en un bloque de código YAML.
|
|
316
|
+
4. Explica brevemente las decisiones no obvias (ej. por qué un flujo es async y no sync).
|
|
317
|
+
5. Menciona advertencias si detectas módulos muy acoplados, responsabilidades difusas o ciclos potenciales.
|
|
318
|
+
6. Indica el comando siguiente: `eva generate system`.
|
|
319
|
+
7. Procede inmediatamente al **Paso 6** para generar `system/system.md`.
|
|
320
|
+
8. Procede inmediatamente al **Paso 6.5** para generar `system/system.mmd`.
|
|
321
|
+
9. Procede inmediatamente al **Paso 7** para generar `system/{module}.yaml` por cada módulo.
|
|
322
|
+
10. Procede inmediatamente al **Paso 8** para generar `system/{module}.md` por cada módulo.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Paso 6 — Crear system.md
|
|
327
|
+
|
|
328
|
+
Inmediatamente después de guardar el `system.yaml`, crea el archivo `system/system.md` en el **mismo directorio** `system/`.
|
|
329
|
+
|
|
330
|
+
Este documento es la **especificación técnica narrativa** del sistema. Sirve como guía de implementación para los desarrolladores de cada módulo. Debe ser lo suficientemente detallado para que un desarrollador pueda implementar un módulo sin necesidad de hacer preguntas adicionales.
|
|
331
|
+
|
|
332
|
+
### Estructura obligatoria del system.md
|
|
333
|
+
|
|
334
|
+
Genera una sección `##` por cada módulo declarado en `modules:`, con las subsecciones siguientes:
|
|
335
|
+
|
|
336
|
+
```markdown
|
|
337
|
+
# system.md — Especificación técnica del sistema
|
|
338
|
+
|
|
339
|
+
## {nombre-del-modulo}
|
|
340
|
+
|
|
341
|
+
### Rol del módulo
|
|
342
|
+
[3–5 párrafos MUY DETALLADOS]
|
|
343
|
+
- Qué problema de negocio resuelve y qué entidades/conceptos gestiona
|
|
344
|
+
- Cuáles son sus responsabilidades exclusivas (límites del bounded context)
|
|
345
|
+
- Qué NO es responsabilidad de este módulo (evitar ambigüedades con otros módulos)
|
|
346
|
+
- Cómo colabora con los otros módulos del sistema
|
|
347
|
+
- Invariantes de negocio que este módulo protege
|
|
348
|
+
|
|
349
|
+
### Casos de uso
|
|
350
|
+
[Un apartado ### por cada useCase declarado en exposes: y en consumers[].useCase]
|
|
351
|
+
|
|
352
|
+
#### {NombreDelUseCase}
|
|
353
|
+
**Qué hace:** [descripción detallada de la lógica de negocio que debe implementar]
|
|
354
|
+
**Precondiciones:** [qué debe ser verdad antes de ejecutarse; entidades que deben existir, estados válidos]
|
|
355
|
+
**Postcondiciones:** [qué estado queda en el sistema tras la ejecución exitosa]
|
|
356
|
+
**Validaciones y errores:** [qué condiciones lanzan excepción y qué tipo de error]
|
|
357
|
+
**Eventos que emite:** [nombre del DomainEvent y cuándo se dispara, o "ninguno"]
|
|
358
|
+
|
|
359
|
+
### Endpoints expuestos
|
|
360
|
+
[Un apartado ### por cada endpoint en exposes:]
|
|
361
|
+
|
|
362
|
+
#### {METHOD} {/path}
|
|
363
|
+
**Propósito:** [descripción del endpoint y su contexto de uso]
|
|
364
|
+
**Path params / Query params:** [descripción de cada parámetro]
|
|
365
|
+
**Request body:** [campos esperados, tipos, restricciones de validación]
|
|
366
|
+
**Response:** [campos devueltos y su significado de negocio]
|
|
367
|
+
**Errores:** [HTTP status codes posibles y cuándo ocurren]
|
|
368
|
+
|
|
369
|
+
### Eventos emitidos
|
|
370
|
+
[Solo si el módulo es producer en integrations.async]
|
|
371
|
+
|
|
372
|
+
#### {NombreDelEvento}
|
|
373
|
+
**Cuándo:** [condición exacta de negocio que dispara el evento]
|
|
374
|
+
**Payload:** [campos del evento con descripción de cada uno]
|
|
375
|
+
**Consumidores y sus acciones:** [módulo → useCase que ejecuta, con descripción de qué hace]
|
|
376
|
+
|
|
377
|
+
### Puertos (llamadas síncronas salientes)
|
|
378
|
+
[Solo si el módulo aparece como caller en integrations.sync]
|
|
379
|
+
|
|
380
|
+
#### {NombreDelPort} → {modulo-destino}
|
|
381
|
+
**Cuándo se llama:** [en qué caso de uso y bajo qué condición]
|
|
382
|
+
**Endpoints usados:** [lista de METHOD /path]
|
|
383
|
+
**Datos que obtiene y cómo los usa:** [descripción detallada]
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Reglas del system.md
|
|
387
|
+
|
|
388
|
+
- **Ser SUMAMENTE específico**: mencionar estados de entidades, validaciones concretas, campos relevantes. Nunca frases como "gestiona los datos" o "maneja el proceso".
|
|
389
|
+
- **Describir flujos de extremo a extremo**: si `ConfirmReservation` es disparado por `PaymentApprovedEvent`, explicarlo en ambas secciones (evento en payments y caso de uso en reservations).
|
|
390
|
+
- **Incluir los `useCase` de consumers como casos de uso del módulo consumidor**: un `useCase` declarado en `consumers[]` debe aparecer como apartado `####` dentro de la sección "Casos de uso" del módulo consumidor, con toda la descripción correspondiente.
|
|
391
|
+
- **Referenciar módulos por nombre** al explicar dependencias y flujos.
|
|
392
|
+
- **Mencionar máquinas de estado** cuando el módulo gestiona ciclos de vida (ej: PENDING → CONFIRMED → SHIPPED).
|
|
393
|
+
- Omitir secciones que no aplican (sin "Puertos" si el módulo no tiene llamadas síncronas salientes; sin "Eventos emitidos" si no produce eventos).
|
|
394
|
+
|
|
395
|
+
### Ejemplo condensado
|
|
396
|
+
|
|
397
|
+
```markdown
|
|
398
|
+
## payments
|
|
399
|
+
|
|
400
|
+
### Rol del módulo
|
|
401
|
+
|
|
402
|
+
El módulo `payments` es el único responsable del ciclo de vida de los pagos vinculados
|
|
403
|
+
a reservas. Gestiona la comunicación con la pasarela de pago externa, registra el
|
|
404
|
+
estado de cada transacción y decide cuándo emitir los eventos que desencadenan
|
|
405
|
+
cambios en otros módulos. No conoce la lógica de reservas ni de notificaciones:
|
|
406
|
+
se limita a recibir una instrucción de cobro, coordinar con la pasarela y reportar
|
|
407
|
+
el resultado mediante eventos de dominio.
|
|
408
|
+
|
|
409
|
+
No es responsabilidad de este módulo confirmar reservas ni enviar notificaciones;
|
|
410
|
+
esos flujos son iniciados por los eventos que payments emite.
|
|
411
|
+
|
|
412
|
+
### Casos de uso
|
|
413
|
+
|
|
414
|
+
#### InitiatePayment
|
|
415
|
+
**Qué hace:** Crea un registro de pago en estado PENDING asociado a una reserva específica.
|
|
416
|
+
Consulta el monto total de la reserva a través del puerto ReservationService y delega
|
|
417
|
+
el cobro a la pasarela externa.
|
|
418
|
+
**Precondiciones:** La reserva debe existir y estar en estado PENDING_PAYMENT. No debe
|
|
419
|
+
existir ya un pago activo para esa reserva.
|
|
420
|
+
**Postcondiciones:** Pago creado en estado PENDING con referencia a la pasarela.
|
|
421
|
+
**Validaciones y errores:** 404 si la reserva no existe; 409 si ya hay un pago activo.
|
|
422
|
+
**Eventos que emite:** ninguno directamente; la pasarela notifica asíncronamente.
|
|
423
|
+
|
|
424
|
+
#### HandleReservationCreated
|
|
425
|
+
**Qué hace:** Al recibir el evento ReservationCreatedEvent, extrae el reservationId
|
|
426
|
+
y el totalAmount del payload e invoca internamente InitiatePayment para iniciar
|
|
427
|
+
el cobro de forma automática sin intervención del usuario.
|
|
428
|
+
**Precondiciones:** El payload debe contener reservationId válido y totalAmount > 0.
|
|
429
|
+
**Postcondiciones:** Se crea un pago PENDING y la pasarela queda en espera de confirmación.
|
|
430
|
+
**Validaciones y errores:** Si InitiatePayment falla, el error se registra en el log
|
|
431
|
+
y el evento se reencola para reintento.
|
|
432
|
+
**Eventos que emite:** ninguno adicional.
|
|
433
|
+
|
|
434
|
+
### Endpoints expuestos
|
|
435
|
+
|
|
436
|
+
#### POST /payments
|
|
437
|
+
**Propósito:** Punto de entrada REST para iniciar un pago manualmente desde integraciones externas.
|
|
438
|
+
**Request body:** reservationId (String, obligatorio), paymentMethod (CARD | CASH, obligatorio).
|
|
439
|
+
**Response:** paymentId, status (PENDING), amount, currency, createdAt.
|
|
440
|
+
**Errores:** 404 si la reserva no existe; 409 si la reserva ya tiene un pago activo.
|
|
441
|
+
|
|
442
|
+
### Eventos emitidos
|
|
443
|
+
|
|
444
|
+
#### PaymentApprovedEvent
|
|
445
|
+
**Cuándo:** La pasarela confirma la aprobación del cobro y se ejecuta ApprovePayment.
|
|
446
|
+
**Payload:** paymentId, reservationId, amount, approvedAt.
|
|
447
|
+
**Consumidores y sus acciones:**
|
|
448
|
+
- reservations → ConfirmReservation: confirma la reserva y la mueve a estado CONFIRMED
|
|
449
|
+
- notifications → NotifyPaymentApproved: envía email y SMS al cliente con el resumen
|
|
450
|
+
|
|
451
|
+
### Puertos (llamadas síncronas salientes)
|
|
452
|
+
|
|
453
|
+
#### ReservationService → reservations
|
|
454
|
+
**Cuándo se llama:** Durante InitiatePayment para obtener el monto total de la reserva.
|
|
455
|
+
**Endpoints usados:** GET /reservations/{id}
|
|
456
|
+
**Datos que obtiene y cómo los usa:** Obtiene totalAmount y customerId para registrar
|
|
457
|
+
el pago con el monto correcto y asociarlo al cliente.
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Paso 6.5 — Crear diagrama de componentes (`system/system.mmd`)
|
|
465
|
+
|
|
466
|
+
Inmediatamente después de guardar `system/system.md`, crea el archivo `system/system.mmd` en el **mismo directorio** `system/`.
|
|
467
|
+
|
|
468
|
+
Este archivo es el **diagrama de componentes Mermaid** del sistema. Es una representación visual directa de la arquitectura descrita en `system.md`: muestra cada módulo como un componente, los actores externos que los consumen, los flujos asíncronos vía broker y las llamadas síncronas HTTP entre módulos.
|
|
469
|
+
|
|
470
|
+
### Estructura del diagrama
|
|
471
|
+
|
|
472
|
+
Usa el tipo `C4Component` si el sistema tiene más de 4 módulos o llamadas externas relevantes; usa `graph LR` para sistemas más simples. La convención preferida es `graph LR` con subgraphs y estilos explícitos:
|
|
473
|
+
|
|
474
|
+
```mermaid
|
|
475
|
+
graph LR
|
|
476
|
+
%% External actors
|
|
477
|
+
Client(["👤 Client"])
|
|
478
|
+
|
|
479
|
+
%% Modules — one subgraph per module
|
|
480
|
+
subgraph orders["orders"]
|
|
481
|
+
ORD_API["REST API"]
|
|
482
|
+
ORD_UC["Use Cases"]
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
subgraph payments["payments"]
|
|
486
|
+
PAY_API["REST API"]
|
|
487
|
+
PAY_UC["Use Cases"]
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
subgraph notifications["notifications"]
|
|
491
|
+
NOT_UC["Use Cases"]
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
%% Message broker (solo si hay mensajería async)
|
|
495
|
+
BROKER[["📨 Kafka"]]
|
|
496
|
+
|
|
497
|
+
%% External services (solo si hay ports)
|
|
498
|
+
EXT_GW[/"💳 PaymentGateway"/]
|
|
499
|
+
|
|
500
|
+
%% HTTP connections — sync
|
|
501
|
+
Client -->|"REST"| ORD_API
|
|
502
|
+
Client -->|"REST"| PAY_API
|
|
503
|
+
ORD_UC -->|"CustomerService\nGET /customers/{id}"| CUST_API
|
|
504
|
+
|
|
505
|
+
%% Async event flows — producer → broker → consumer(s)
|
|
506
|
+
ORD_UC -->|"OrderPlacedEvent"| BROKER
|
|
507
|
+
BROKER -->|"OrderPlacedEvent"| PAY_UC
|
|
508
|
+
BROKER -->|"OrderPlacedEvent"| NOT_UC
|
|
509
|
+
PAY_UC -->|"PaymentApprovedEvent"| BROKER
|
|
510
|
+
BROKER -->|"PaymentApprovedEvent"| ORD_UC
|
|
511
|
+
|
|
512
|
+
%% External port calls
|
|
513
|
+
PAY_UC -->|"processPayment"| EXT_GW
|
|
514
|
+
|
|
515
|
+
%% Styles
|
|
516
|
+
classDef module fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
|
|
517
|
+
classDef broker fill:#fef9c3,stroke:#ca8a04,color:#713f12
|
|
518
|
+
classDef actor fill:#f0fdf4,stroke:#16a34a,color:#14532d
|
|
519
|
+
classDef external fill:#fce7f3,stroke:#db2777,color:#831843
|
|
520
|
+
class orders,payments,notifications module
|
|
521
|
+
class BROKER broker
|
|
522
|
+
class Client actor
|
|
523
|
+
class EXT_GW external
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Reglas del diagrama de componentes
|
|
527
|
+
|
|
528
|
+
- **Un subgraph por módulo** — el nombre del subgraph coincide exactamente con el nombre del módulo en `system.yaml`
|
|
529
|
+
- **Actores externos** (`Client`, `Admin`, etc.) como nodos `(["..."])` fuera de los subgraphs — solo los que interactúan directamente con la API
|
|
530
|
+
- **Broker** como nodo `[["..."`]] central — solo si `messaging.enabled: true`; omitir si no hay async
|
|
531
|
+
- **Servicios externos** vía `ports:` como nodos `[/"..."/]` — uno por `service:` único en `integrations.sync`
|
|
532
|
+
- **Flechas sync HTTP** (`-->|"PortName\nMETHOD /path"|`) entre módulo caller y módulo/servicio callee
|
|
533
|
+
- **Flechas async** (`-->|"EventName"|`) siempre pasan por el broker: `producer → BROKER → consumer`; nunca directo
|
|
534
|
+
- **Estilos obligatorios**: usar `classDef` para diferenciar visualmente módulos, broker, actores y servicios externos
|
|
535
|
+
- **Todo en inglés**: labels de flechas, nombres de nodos, comments
|
|
536
|
+
- **El archivo no contiene nada más que el bloque Mermaid** — sin frontmatter, sin texto markdown, solo el diagrama
|
|
537
|
+
|
|
538
|
+
### Correspondencia con system.md
|
|
539
|
+
|
|
540
|
+
El diagrama es una proyección visual 1:1 de lo descrito en `system.md`:
|
|
541
|
+
|
|
542
|
+
| Elemento en system.md | Elemento en system.mmd |
|
|
543
|
+
|---|---|
|
|
544
|
+
| Módulo con endpoints REST | Subgraph con nodo `REST API` interno |
|
|
545
|
+
| Módulo sin endpoints | Subgraph sin nodo REST |
|
|
546
|
+
| `integrations.async[].producer` → broker | Flecha `producer → BROKER` |
|
|
547
|
+
| `integrations.async[].consumers[]` | Flechas `BROKER → consumer` (una por consumidor) |
|
|
548
|
+
| `integrations.sync[].caller` → `calls` | Flecha directa `caller → target` con label del port |
|
|
549
|
+
| `integrations.sync[].calls` (externo) | Nodo `[/"ExtSvc"/]` fuera de subgraphs |
|
|
550
|
+
| Actor que llama endpoints | Nodo `Client` con flecha REST al módulo |
|
|
551
|
+
|
|
552
|
+
### Checklist del system.mmd
|
|
553
|
+
|
|
554
|
+
Antes de guardar, verifica:
|
|
555
|
+
|
|
556
|
+
- [ ] Un subgraph por cada módulo en `modules:`
|
|
557
|
+
- [ ] Flechas async pasan siempre por el nodo BROKER
|
|
558
|
+
- [ ] Flechas sync son directas entre módulos (no pasan por broker)
|
|
559
|
+
- [ ] Servicios externos tienen nodo propio fuera de subgraphs
|
|
560
|
+
- [ ] `classDef` aplicado a todos los nodos relevantes
|
|
561
|
+
- [ ] Todo el contenido en inglés
|
|
562
|
+
- [ ] El archivo contiene **solo** el bloque Mermaid (sin markdown alrededor)
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## Paso 7 — Crear archivos de dominio por módulo (`system/{module}.yaml`)
|
|
569
|
+
|
|
570
|
+
Inmediatamente después de crear `system/system.mmd`, genera **un archivo `domain.yaml` por cada módulo** declarado en `modules:`. La ruta es `system/{nombre-del-modulo}.yaml` (mismo directorio `system/`).
|
|
571
|
+
|
|
572
|
+
Este archivo es el **modelo de dominio del módulo**: define las entidades, value objects, enums, relaciones, eventos y configuraciones de infraestructura. Es el input directo para el comando `eva g entities <module>`.
|
|
573
|
+
|
|
574
|
+
> **Referencia técnica completa:** antes de construir cada archivo, lee `references/GENERATE_ENTITIES.md` para verificar qué propiedades son válidas y qué código produce cada configuración.
|
|
575
|
+
|
|
576
|
+
### Rol de experto de dominio por módulo
|
|
577
|
+
|
|
578
|
+
Al construir el `system/{module}.yaml` de cada módulo, **activa el rol de experto en el dominio específico de ese módulo**. Esto significa razonar como alguien que conoce en profundidad el negocio de ese bounded context en particular:
|
|
579
|
+
|
|
580
|
+
- **`orders`** → piensa como un experto en gestión de pedidos: ciclos de vida, estados válidos, invariantes de negocio (no se puede confirmar un pedido ya cancelado), relaciones con items, totales calculados
|
|
581
|
+
- **`payments`** → piensa como un experto en pagos: métodos de pago, reintentos, estados terminales, reconciliación, prevención de doble cobro
|
|
582
|
+
- **`inventory`** → piensa como un experto en inventario: stock disponible vs. reservado, movimientos, alertas de reposición
|
|
583
|
+
- **`notifications`** → piensa como un experto en comunicaciones: canales (email, SMS, push), plantillas, idempotencia, reintentos
|
|
584
|
+
|
|
585
|
+
Este conocimiento aplicado te permite:
|
|
586
|
+
- Proponer **campos que el usuario no mencionó** pero son necesarios para el dominio (ej: `cancelledReason` en un módulo de pedidos)
|
|
587
|
+
- Sugerir **Value Objects** que mejoran la expresividad (ej: `Money` para importes, `Address` para direcciones)
|
|
588
|
+
- Reconocer **invariantes implícitas** del dominio (ej: el stock no puede ser negativo)
|
|
589
|
+
- Modelar **transiciones de estado** completas y realistas para el ciclo de vida de la entidad
|
|
590
|
+
- Identificar qué campos deben ser `readOnly` (calculados), `hidden` (sensibles) o tener `defaultValue`
|
|
591
|
+
|
|
592
|
+
> **Cuándo preguntar:** si el conocimiento general del dominio no es suficiente para tomar una decisión de modelado — por ejemplo, reglas de negocio específicas de la empresa (¿se permiten pedidos con cantidad 0? ¿cuántos días para cancelar?) — pregunta en un solo mensaje antes de continuar con ese módulo.
|
|
593
|
+
|
|
594
|
+
### Restricciones absolutas al construir el domain.yaml
|
|
595
|
+
|
|
596
|
+
Antes de escribir cualquier YAML, verifica que NO estás cometiendo ninguno de estos errores:
|
|
597
|
+
|
|
598
|
+
1. ❌ **No agregar `@ManyToOne` / `@OneToMany` entre agregados distintos** — las referencias cross-aggregate son IDs simples con `reference:`
|
|
599
|
+
2. ❌ **No incluir campos de auditoría en `fields:`** (`createdAt`, `updatedAt`, `createdBy`, `updatedBy`) — los genera la infraestructura automáticamente con `audit.enabled: true`
|
|
600
|
+
3. ❌ **No usar `defaultValue` en campos que no son `readOnly: true`**
|
|
601
|
+
4. ❌ **No declarar `transitions` sin `initialValue` en el enum** — si hay lifecycle, declarar ambos
|
|
602
|
+
5. ❌ **No inventar módulos en `reference.module`** — solo los declarados en `system/system.yaml → modules:`
|
|
603
|
+
6. ❌ **No duplicar en `endpoints:`** lo que ya está en `system/system.yaml → exposes:`
|
|
604
|
+
12. ❌ **`endpoints:` NUNCA es una lista plana** — usar siempre la estructura `{ basePath, versions: [{ version, operations: [...] }] }`. Una lista plana hace que el generador no produzca ningún Command, Handler ni Controller.
|
|
605
|
+
7. ❌ **No inventar eventos en `events:`** — deben coincidir con `integrations.async[]` donde `producer` es este módulo. ✅ **Declarar siempre `topic:` en cada evento** con el valor exacto de `integrations.async[].topic` — aunque el generador puede derivarlo automáticamente, la declaración explícita garantiza consistencia con los `listeners[]` de los módulos consumidores
|
|
606
|
+
8. ❌ **No inventar listeners** — deben coincidir con `integrations.async[].consumers[]` donde `module` es este módulo
|
|
607
|
+
9. ❌ **No inventar ports** — deben coincidir con `integrations.sync[]` donde `caller` es este módulo
|
|
608
|
+
10. ❌ **Todo el contenido del archivo debe estar en inglés** — field names, comments, descriptions, values
|
|
609
|
+
11. ❌ **No dejar transiciones sin evidencia de activación** — toda transición que se ejecuta como consecuencia de una respuesta de `ports:` (éxito o falla) o de un proceso interno/scheduler **debe** tener un domain event con `triggers` — es el único mecanismo que permite al validador C2-001 reconocerlas como alcanzables
|
|
610
|
+
13. ❌ **No reutilizar el mismo nombre de `service:` en `ports[]` de módulos distintos** — si dos módulos llaman al mismo servicio externo, cada uno debe usar un nombre propio de su bounded context (ej: `OrderCustomerService` en `orders`, `DeliveryCustomerService` en `deliveries`); el mismo nombre produce `ConflictingBeanDefinitionException` en Spring
|
|
611
|
+
|
|
612
|
+
### Inferencia desde system.yaml
|
|
613
|
+
|
|
614
|
+
Para cada módulo, extrae del `system/system.yaml`:
|
|
615
|
+
|
|
616
|
+
| Fuente en system.yaml | Destino en domain.yaml |
|
|
617
|
+
|---|---|
|
|
618
|
+
| `modules[x].exposes[]` | `endpoints:` — objeto con `basePath` + `versions[].operations[]`; **NUNCA** lista plana |
|
|
619
|
+
| `integrations.async[]` donde `producer = módulo` | `events:` (nombres de eventos a producir) |
|
|
620
|
+
| `integrations.async[].consumers[]` donde `module = módulo` | `listeners:` (con `useCase`) |
|
|
621
|
+
| `integrations.sync[]` donde `caller = módulo` | `ports:` (con `service`, `http`, `using`) |
|
|
622
|
+
|
|
623
|
+
> ❌ **Formato ERRADO** — `endpoints:` como lista plana de objetos `{method, path, useCase}` — el generador lo ignora y no produce ningún Command/Handler:
|
|
624
|
+
> ```yaml
|
|
625
|
+
> # ❌ NUNCA así
|
|
626
|
+
> endpoints:
|
|
627
|
+
> - method: POST
|
|
628
|
+
> path: /orders
|
|
629
|
+
> useCase: CreateOrder
|
|
630
|
+
> ```
|
|
631
|
+
> ✅ **Formato CORRECTO** — objeto con `basePath` y `versions[].operations[]`:
|
|
632
|
+
> ```yaml
|
|
633
|
+
> # ✅ SIEMPRE así
|
|
634
|
+
> endpoints:
|
|
635
|
+
> basePath: /orders
|
|
636
|
+
> versions:
|
|
637
|
+
> - version: v1
|
|
638
|
+
> operations:
|
|
639
|
+
> - useCase: CreateOrder
|
|
640
|
+
> method: POST
|
|
641
|
+
> path: /
|
|
642
|
+
> ```
|
|
643
|
+
|
|
644
|
+
### Estructura completa del `system/{module}.yaml`
|
|
645
|
+
|
|
646
|
+
```yaml
|
|
647
|
+
aggregates:
|
|
648
|
+
- name: Order # PascalCase — nombre del agregado
|
|
649
|
+
entities:
|
|
650
|
+
- name: order # camelCase — nombre de la entidad raíz
|
|
651
|
+
isRoot: true
|
|
652
|
+
tableName: orders # snake_case — nombre de tabla SQL
|
|
653
|
+
hasSoftDelete: false # true para borrado lógico (deletedAt)
|
|
654
|
+
audit:
|
|
655
|
+
enabled: true # adds createdAt, updatedAt
|
|
656
|
+
trackUser: false # adds createdBy, updatedBy
|
|
657
|
+
fields:
|
|
658
|
+
- name: id
|
|
659
|
+
type: String
|
|
660
|
+
- name: orderNumber
|
|
661
|
+
type: String
|
|
662
|
+
validations:
|
|
663
|
+
- type: NotBlank
|
|
664
|
+
message: "Order number is required"
|
|
665
|
+
- name: totalAmount
|
|
666
|
+
type: BigDecimal
|
|
667
|
+
readOnly: true # calculated — excluded from CreateDto and business constructor
|
|
668
|
+
defaultValue: "0.00" # initial value in creation constructor
|
|
669
|
+
- name: status
|
|
670
|
+
type: OrderStatus
|
|
671
|
+
readOnly: true # managed by transitions
|
|
672
|
+
- name: processingToken
|
|
673
|
+
type: String
|
|
674
|
+
hidden: true # sensitive — excluded from ResponseDto
|
|
675
|
+
- name: customerId
|
|
676
|
+
type: String
|
|
677
|
+
reference:
|
|
678
|
+
aggregate: Customer # PascalCase
|
|
679
|
+
module: customers # module where the aggregate lives
|
|
680
|
+
relationships:
|
|
681
|
+
- type: OneToMany
|
|
682
|
+
target: OrderItem
|
|
683
|
+
mappedBy: order
|
|
684
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
685
|
+
fetch: LAZY
|
|
686
|
+
|
|
687
|
+
- name: orderItem # secondary entity — isRoot omitted (false by default)
|
|
688
|
+
tableName: order_items
|
|
689
|
+
fields:
|
|
690
|
+
- name: id
|
|
691
|
+
type: String
|
|
692
|
+
- name: productId
|
|
693
|
+
type: String
|
|
694
|
+
- name: quantity
|
|
695
|
+
type: Integer
|
|
696
|
+
validations:
|
|
697
|
+
- type: Min
|
|
698
|
+
value: 1
|
|
699
|
+
- name: unitPrice
|
|
700
|
+
type: BigDecimal
|
|
701
|
+
# Do NOT declare ManyToOne toward Order — the generator infers it automatically
|
|
702
|
+
# from the OneToMany declared in the root entity
|
|
703
|
+
|
|
704
|
+
valueObjects:
|
|
705
|
+
- name: ShippingAddress # PascalCase — immutable, no ID
|
|
706
|
+
fields:
|
|
707
|
+
- name: street
|
|
708
|
+
type: String
|
|
709
|
+
- name: city
|
|
710
|
+
type: String
|
|
711
|
+
- name: zipCode
|
|
712
|
+
type: String
|
|
713
|
+
methods: # optional — business logic of the VO
|
|
714
|
+
- name: format
|
|
715
|
+
returnType: String
|
|
716
|
+
parameters: []
|
|
717
|
+
body: "return street + \", \" + city + \" \" + zipCode;"
|
|
718
|
+
|
|
719
|
+
enums:
|
|
720
|
+
- name: OrderStatus
|
|
721
|
+
initialValue: PENDING # initial state — excluded from CreateDto
|
|
722
|
+
transitions:
|
|
723
|
+
- from: PENDING
|
|
724
|
+
to: CONFIRMED
|
|
725
|
+
method: confirm
|
|
726
|
+
- from: CONFIRMED
|
|
727
|
+
to: SHIPPED
|
|
728
|
+
method: ship
|
|
729
|
+
- from: [PENDING, CONFIRMED] # multiple origins allowed
|
|
730
|
+
to: CANCELLED
|
|
731
|
+
method: cancel
|
|
732
|
+
guard: "this.status == OrderStatus.DELIVERED" # throws BusinessException if true
|
|
733
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
734
|
+
|
|
735
|
+
events:
|
|
736
|
+
- name: OrderPlacedEvent # PascalCase, past tense, Event suffix
|
|
737
|
+
topic: ORDER_PLACED # preferred: explicit topic — must match listeners[].topic in consumers
|
|
738
|
+
triggers:
|
|
739
|
+
- confirm # transition method name that fires this event
|
|
740
|
+
fields:
|
|
741
|
+
# Declare {entityName}Id when the event crosses module boundaries via Kafka
|
|
742
|
+
# — the generator maps it to event.getAggregateId() in the handler
|
|
743
|
+
- name: customerId
|
|
744
|
+
type: String
|
|
745
|
+
- name: confirmedAt # ends in 'At' + LocalDateTime → resolves to LocalDateTime.now()
|
|
746
|
+
type: LocalDateTime
|
|
747
|
+
- name: OrderCancelledEvent
|
|
748
|
+
topic: ORDER_CANCELLED # preferred: explicit topic
|
|
749
|
+
triggers:
|
|
750
|
+
- cancel
|
|
751
|
+
fields:
|
|
752
|
+
- name: reason # unresolved → null /* TODO: provide reason */
|
|
753
|
+
type: String
|
|
754
|
+
|
|
755
|
+
# Declarative REST endpoints — inferred from system/system.yaml → exposes:
|
|
756
|
+
endpoints:
|
|
757
|
+
basePath: /orders
|
|
758
|
+
versions:
|
|
759
|
+
- version: v1
|
|
760
|
+
operations:
|
|
761
|
+
- useCase: GetOrder
|
|
762
|
+
method: GET
|
|
763
|
+
path: /{id} # relative to basePath
|
|
764
|
+
- useCase: FindAllOrders
|
|
765
|
+
method: GET
|
|
766
|
+
path: /
|
|
767
|
+
- useCase: CreateOrder
|
|
768
|
+
method: POST
|
|
769
|
+
path: /
|
|
770
|
+
- useCase: ConfirmOrder # business name → generates scaffold
|
|
771
|
+
method: PUT
|
|
772
|
+
path: /{id}/confirm
|
|
773
|
+
|
|
774
|
+
# External events this module CONSUMES
|
|
775
|
+
# Inferred from system/system.yaml → integrations.async[].consumers[] where module = this module
|
|
776
|
+
listeners:
|
|
777
|
+
- event: PaymentApprovedEvent
|
|
778
|
+
producer: payments
|
|
779
|
+
topic: PAYMENT_APPROVED
|
|
780
|
+
useCase: ConfirmOrder # must match consumers[].useCase in system.yaml
|
|
781
|
+
fields:
|
|
782
|
+
- name: orderId
|
|
783
|
+
type: String
|
|
784
|
+
- name: approvedAt
|
|
785
|
+
type: LocalDateTime
|
|
786
|
+
- name: paymentDetails # object type → MUST declare in nestedTypes:
|
|
787
|
+
type: PaymentDetails
|
|
788
|
+
nestedTypes:
|
|
789
|
+
- name: paymentDetails # camelCase → generates PaymentDetails record
|
|
790
|
+
fields:
|
|
791
|
+
- name: paymentId
|
|
792
|
+
type: String
|
|
793
|
+
- name: amount
|
|
794
|
+
type: BigDecimal
|
|
795
|
+
|
|
796
|
+
# Synchronous outbound HTTP services this module CALLS
|
|
797
|
+
# Inferred from system/system.yaml → integrations.sync[] where caller = this module
|
|
798
|
+
ports:
|
|
799
|
+
- name: findCustomerById
|
|
800
|
+
service: CustomerService # must match integrations.sync[].port
|
|
801
|
+
target: customers
|
|
802
|
+
baseUrl: http://localhost:8080 # only on the first entry per service
|
|
803
|
+
http: GET /customers/{id}
|
|
804
|
+
fields:
|
|
805
|
+
- name: id
|
|
806
|
+
type: String
|
|
807
|
+
- name: email
|
|
808
|
+
type: String
|
|
809
|
+
- name: processPayment
|
|
810
|
+
service: PaymentGateway
|
|
811
|
+
target: payments
|
|
812
|
+
baseUrl: https://api.payments.example.com
|
|
813
|
+
http: POST /payments
|
|
814
|
+
body:
|
|
815
|
+
- name: reservationId
|
|
816
|
+
type: String
|
|
817
|
+
- name: paymentMethod
|
|
818
|
+
type: PaymentMethodInput # object in body → declare in nestedTypes:
|
|
819
|
+
nestedTypes:
|
|
820
|
+
- name: paymentMethodInput
|
|
821
|
+
fields:
|
|
822
|
+
- name: type
|
|
823
|
+
type: String
|
|
824
|
+
- name: cardToken
|
|
825
|
+
type: String
|
|
826
|
+
fields:
|
|
827
|
+
- name: paymentId
|
|
828
|
+
type: String
|
|
829
|
+
- name: status
|
|
830
|
+
type: String
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Tabla de visibilidad de campos
|
|
834
|
+
|
|
835
|
+
| Configuración | Business constructor | CreateDto | ResponseDto |
|
|
836
|
+
|---|---|---|---|
|
|
837
|
+
| Normal field | ✅ | ✅ | ✅ |
|
|
838
|
+
| `readOnly: true` | ❌ | ❌ | ✅ |
|
|
839
|
+
| `readOnly: true` + `defaultValue` | ⚡ assigned with default | ❌ | ✅ |
|
|
840
|
+
| `hidden: true` | ✅ | ✅ | ❌ |
|
|
841
|
+
| Both flags | ❌ | ❌ | ❌ |
|
|
842
|
+
|
|
843
|
+
### Tipos de datos soportados
|
|
844
|
+
|
|
845
|
+
| YAML type | Java type |
|
|
846
|
+
|---|---|
|
|
847
|
+
| `String` | String |
|
|
848
|
+
| `Integer` | Integer |
|
|
849
|
+
| `Long` | Long |
|
|
850
|
+
| `BigDecimal` | BigDecimal |
|
|
851
|
+
| `Boolean` | Boolean |
|
|
852
|
+
| `LocalDate` | LocalDate |
|
|
853
|
+
| `LocalDateTime` | LocalDateTime |
|
|
854
|
+
| `UUID` | UUID |
|
|
855
|
+
|
|
856
|
+
### Reglas de relaciones
|
|
857
|
+
|
|
858
|
+
- ✅ `OneToMany` / `OneToOne` entre entidades del **mismo agregado** → declarar solo en la entidad raíz
|
|
859
|
+
- ✅ El generador infiere automáticamente el lado inverso (`ManyToOne`) — **no declararlo en la secundaria**
|
|
860
|
+
- ✅ Referencia a entidad de **otro agregado** → usar `reference:` en el campo ID, nunca `relationships:`
|
|
861
|
+
- El `mappedBy` debe coincidir con el nombre del campo en la entidad secundaria que apunta de vuelta a la raíz
|
|
862
|
+
|
|
863
|
+
### Proyecciones locales (read models sincronizados por eventos)
|
|
864
|
+
|
|
865
|
+
Cuando un módulo consume eventos de otro módulo para mantener una **copia local desnormalizada** (proyección / read model), ese agregado tiene características distintas al dominio principal.
|
|
866
|
+
|
|
867
|
+
**Señales de que un agregado es una proyección local:**
|
|
868
|
+
- Sus campos coinciden con los `fields[]` de uno o más listeners
|
|
869
|
+
- Su useCase principal es `Register*InLocalCatalog`, `Sync*` o `Update*FromEvent`
|
|
870
|
+
- No tiene endpoints HTTP de escritura — solo se actualiza por listeners Kafka
|
|
871
|
+
- Existe para evitar llamadas síncronas al módulo origen en tiempo real
|
|
872
|
+
|
|
873
|
+
**Reglas de auditoría para proyecciones:**
|
|
874
|
+
|
|
875
|
+
| Tipo de entidad | `audit.enabled` | `audit.trackUser` | Justificación |
|
|
876
|
+
|---|---|---|---|
|
|
877
|
+
| Entidad de dominio principal | ✅ true | según necesidad | Trazabilidad completa requerida |
|
|
878
|
+
| Proyección local (read model) | ✅ true | ❌ false | `createdAt`/`updatedAt` permite depurar desincronizaciones; `trackUser: false` porque los cambios vienen de eventos, no de usuarios |
|
|
879
|
+
|
|
880
|
+
> **Error común:** omitir `audit.enabled` en proyecciones porque "no son datos propios". En módulos críticos (reservations, payments, orders) las proyecciones afectan directamente al negocio — saber cuándo cambió `isAvailable` o `accountStatus` es esencial para depurar problemas de sincronización en producción.
|
|
881
|
+
|
|
882
|
+
**Patrón canónico de proyección local:**
|
|
883
|
+
|
|
884
|
+
```yaml
|
|
885
|
+
- name: BikeCatalog # nombre del agregado proyección
|
|
886
|
+
entities:
|
|
887
|
+
- name: bikeCatalogEntry
|
|
888
|
+
isRoot: true
|
|
889
|
+
tableName: bike_catalog_entries
|
|
890
|
+
audit:
|
|
891
|
+
enabled: true # ✅ siempre true en módulos críticos
|
|
892
|
+
trackUser: false # ✅ cambios vienen de eventos, no de usuarios
|
|
893
|
+
fields:
|
|
894
|
+
- name: id
|
|
895
|
+
type: String
|
|
896
|
+
- name: isAvailable
|
|
897
|
+
type: Boolean
|
|
898
|
+
readOnly: true
|
|
899
|
+
defaultValue: true # estado inicial al registrarse
|
|
900
|
+
|
|
901
|
+
# Los listeners que mantienen esta proyección:
|
|
902
|
+
listeners:
|
|
903
|
+
- event: BikeCreatedEvent
|
|
904
|
+
producer: fleet
|
|
905
|
+
topic: BIKE_CREATED
|
|
906
|
+
useCase: RegisterBikeInLocalCatalog # crea la entrada en la proyección
|
|
907
|
+
fields:
|
|
908
|
+
- name: code
|
|
909
|
+
type: String
|
|
910
|
+
- name: stationId
|
|
911
|
+
type: String
|
|
912
|
+
|
|
913
|
+
- event: BikeMaintenanceStartedEvent
|
|
914
|
+
producer: fleet
|
|
915
|
+
topic: BIKE_MAINTENANCE_STARTED
|
|
916
|
+
useCase: MarkBikeUnavailable # actualiza isAvailable=false
|
|
917
|
+
fields:
|
|
918
|
+
- name: stationId
|
|
919
|
+
type: String
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
### Clasificación de campos `readOnly` por origen
|
|
923
|
+
|
|
924
|
+
Antes de declarar los `events:`, clasifica cada campo `readOnly` de la entidad raíz según **cuándo y cómo se asigna su valor**. Esta clasificación determina en qué evento debe aparecer:
|
|
925
|
+
|
|
926
|
+
| Categoría | Cuándo se asigna | Debe aparecer en... |
|
|
927
|
+
|---|---|---|
|
|
928
|
+
| **Constante del sistema** | Constructor con `defaultValue` | Ningún evento — es configuración |
|
|
929
|
+
| **Estado de máquina** | Cada transición (enum con `transitions`) | Ningún campo explícito — el nombre del evento comunica el estado |
|
|
930
|
+
| **Timestamp de transición** | Una transición específica (`pickup()`, `complete()`…) | El evento de **esa** transición via `triggers` |
|
|
931
|
+
| **Dato calculado de transición** | Se calcula al ejecutar una transición | El evento de esa transición via `triggers` |
|
|
932
|
+
| **Acumulador** | Se actualiza en múltiples operaciones | En el evento de cada operación que lo modifica |
|
|
933
|
+
|
|
934
|
+
**Patrón más propenso a omisiones — timestamp de transición:**
|
|
935
|
+
|
|
936
|
+
Campos como `actualPickupAt`, `completedAt`, `processedAt`, `sentAt` se asignan en el momento exacto de una transición. Son la **evidencia temporal del hecho ocurrido** y deben incluirse en el evento de esa transición:
|
|
937
|
+
|
|
938
|
+
```yaml
|
|
939
|
+
# ❌ MAL — actualPickupAt se asigna en pickup() pero no hay evento para esa transición
|
|
940
|
+
fields:
|
|
941
|
+
- name: actualPickupAt
|
|
942
|
+
type: LocalDateTime
|
|
943
|
+
readOnly: true
|
|
944
|
+
enums:
|
|
945
|
+
- name: ReservationStatus
|
|
946
|
+
transitions:
|
|
947
|
+
- from: CONFIRMED
|
|
948
|
+
to: IN_PROGRESS
|
|
949
|
+
method: pickup # asigna actualPickupAt pero no tiene evento asociado
|
|
950
|
+
# events: no incluye ningún evento con triggers: [pickup]
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
```yaml
|
|
954
|
+
# ✅ BIEN — la transición pickup tiene su evento y el timestamp está en los fields
|
|
955
|
+
fields:
|
|
956
|
+
- name: actualPickupAt
|
|
957
|
+
type: LocalDateTime
|
|
958
|
+
readOnly: true
|
|
959
|
+
enums:
|
|
960
|
+
- name: ReservationStatus
|
|
961
|
+
transitions:
|
|
962
|
+
- from: CONFIRMED
|
|
963
|
+
to: IN_PROGRESS
|
|
964
|
+
method: pickup
|
|
965
|
+
events:
|
|
966
|
+
- name: ReservationPickedUpEvent
|
|
967
|
+
triggers:
|
|
968
|
+
- pickup
|
|
969
|
+
fields:
|
|
970
|
+
- name: userId
|
|
971
|
+
type: String
|
|
972
|
+
- name: bikeId
|
|
973
|
+
type: String
|
|
974
|
+
- name: actualPickupAt # timestamp asignado en esta transición
|
|
975
|
+
type: LocalDateTime
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
**Protocolo para cada campo `readOnly` sin `defaultValue` y cuyo tipo no es un enum con `transitions`:**
|
|
979
|
+
|
|
980
|
+
```
|
|
981
|
+
1. ¿En qué transición o caso de uso se asigna este campo?
|
|
982
|
+
2. ¿Esa transición ya tiene un evento con triggers?
|
|
983
|
+
→ Sí: agregar el campo al fields[] de ese evento
|
|
984
|
+
→ No: crear el evento + triggers ANTES de continuar el diseño
|
|
985
|
+
3. Si se asigna en múltiples transiciones: incluirlo en el evento de la transición principal
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
> **Señal de alerta:** una transición que asigna campos `readOnly` (especialmente `*At`) pero no tiene evento asociado es una **brecha de diseño**. Nunca dejes una transición sin evento cuando modifica datos que otros módulos o el propio módulo necesitan trazar.
|
|
989
|
+
|
|
990
|
+
### Análisis de activación de transiciones
|
|
991
|
+
|
|
992
|
+
Antes de escribir los `events:`, recorre **cada método en `transitions[]`** del módulo y clasifícalo según quién lo activa:
|
|
993
|
+
|
|
994
|
+
| ¿Quién activa la transición? | Mecanismo de diseño correcto | C2-001 silenciado por... |
|
|
995
|
+
|---|---|---|
|
|
996
|
+
| Un endpoint HTTP (PUT/POST/PATCH) | `endpoints:` con `useCase` que hace match semántico | Overlap de palabras entre método y useCase |
|
|
997
|
+
| Un listener Kafka | `listeners:` con `useCase` que hace match semántico | Overlap de palabras entre método y useCase |
|
|
998
|
+
| La **respuesta exitosa** de un `ports:` call | Domain event + `triggers: [método]` | Presencia del trigger en `triggeredMethods` |
|
|
999
|
+
| La **respuesta de error/timeout** de un `ports:` call | Domain event + `triggers: [método]` | Presencia del trigger en `triggeredMethods` |
|
|
1000
|
+
| Un scheduler / process interno / batch | Domain event + `triggers: [método]` | Presencia del trigger en `triggeredMethods` |
|
|
1001
|
+
|
|
1002
|
+
**Patrón canonical — respuesta de port con dos ramas (éxito/falla):**
|
|
1003
|
+
|
|
1004
|
+
Cuando el módulo llama a un servicio externo vía `ports:` y el resultado puede ser éxito o falla, las transiciones de esas dos ramas deben modelarse con eventos separados:
|
|
1005
|
+
|
|
1006
|
+
```yaml
|
|
1007
|
+
events:
|
|
1008
|
+
- name: NotificationSentEvent # rama éxito del ports: call
|
|
1009
|
+
triggers:
|
|
1010
|
+
- markAsSent # transition method activado por 2xx
|
|
1011
|
+
fields:
|
|
1012
|
+
- name: sentAt
|
|
1013
|
+
type: LocalDateTime
|
|
1014
|
+
|
|
1015
|
+
- name: NotificationFailedEvent # rama error del ports: call
|
|
1016
|
+
triggers:
|
|
1017
|
+
- markAsFailed # transition method activado por error/timeout
|
|
1018
|
+
fields: [] # puede no necesitar campos extra
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
> **Señal de alerta:** si el módulo tiene `ports:` con respuesta y el enum de estado tiene transiciones de `SUCCESS`/`FAILED` (o equivalentes), y NO hay domain events con `triggers` para ellas → el validador C2-001 las marcará como inalcanzables. La solución siempre es agregar los events, no modificar el validador.
|
|
1022
|
+
|
|
1023
|
+
### Análisis de trazabilidad de enums `*Type`
|
|
1024
|
+
|
|
1025
|
+
Todo enum cuyo nombre termina en `Type` (ej: `IncidentType`, `PaymentType`, `NotificationType`) representa una **clasificación** cuyos valores solo pueden nacer de un mecanismo externo o interno. El validador C2-003 verifica que **cada valor sea semánticamente trazable** a al menos uno de estos orígenes:
|
|
1026
|
+
|
|
1027
|
+
| Origen del valor | Traza semántica | Ejemplo |
|
|
1028
|
+
|---|---|---|
|
|
1029
|
+
| Listener Kafka (nombre del evento) | Tokens del nombre del evento | `TripCompletedEvent` → `trip`, `completed` |
|
|
1030
|
+
| Listener Kafka (nombre de campo) | Tokens del nombre del campo | `wasLateReturn` → `late`, `return` → traza `LATE_RETURN` |
|
|
1031
|
+
| Endpoint HTTP (useCase) | Tokens del nombre del useCase | `RegisterIncident` → `register`, `incident` |
|
|
1032
|
+
| Domain event producido (nombre) | Tokens del nombre del evento | `IncidentRegisteredEvent` → `incident`, `registered` |
|
|
1033
|
+
|
|
1034
|
+
**Protocolo obligatorio — para cada valor de un enum `*Type`:**
|
|
1035
|
+
|
|
1036
|
+
```
|
|
1037
|
+
Para cada valor en {Enum}Type:
|
|
1038
|
+
1. ¿Lo crea un usuario/operador vía HTTP? → declarar endpoint con useCase que contenga
|
|
1039
|
+
palabras del valor (RegisterIncident,
|
|
1040
|
+
ReportDamage, CreatePaymentByCard, etc.)
|
|
1041
|
+
2. ¿Lo origina un evento Kafka entrante? → el nombre del evento o alguno de sus campos
|
|
1042
|
+
debe contener palabras del valor
|
|
1043
|
+
3. ¿Lo origina un domain event producido? → el nombre del evento ya aporta tokens
|
|
1044
|
+
4. Ninguno aplica → valor HUÉRFANO — falta un endpoint o listener;
|
|
1045
|
+
revisar si falta diseño o si el valor sobra
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
**Ejemplo canónico — `IncidentType` con `[LATE_RETURN, DAMAGE_REPORT]`:**
|
|
1049
|
+
|
|
1050
|
+
| Valor | ¿Quién lo origina? | Traza correcta |
|
|
1051
|
+
|---|---|---|
|
|
1052
|
+
| `LATE_RETURN` | Automático: `TripCompletedEvent.wasLateReturn == true` | Campo `wasLateReturn` en `listeners[].fields` → tokens `late`, `return` |
|
|
1053
|
+
| `DAMAGE_REPORT` | Manual: staff reporta daño vía HTTP | Endpoint `POST /{id}/incidents` con `useCase: RegisterIncident` → tokens `register`, `incident` |
|
|
1054
|
+
|
|
1055
|
+
Si `DAMAGE_REPORT` no tiene un endpoint declarado → C2-003 lo marcará huérfano. La solución es **agregar el endpoint**, no ignorar el warning.
|
|
1056
|
+
|
|
1057
|
+
**Patrón `subEntityAdd` para entidades secundarias con `*Type`:**
|
|
1058
|
+
|
|
1059
|
+
Cuando el módulo tiene `OneToMany` hacia una entidad secundaria con un campo `type: FooType` que se crea por HTTP, declarar siempre el endpoint correspondiente:
|
|
1060
|
+
|
|
1061
|
+
```yaml
|
|
1062
|
+
endpoints:
|
|
1063
|
+
basePath: /accounts
|
|
1064
|
+
versions:
|
|
1065
|
+
- version: v1
|
|
1066
|
+
operations:
|
|
1067
|
+
- useCase: RegisterIncident # tokens: register + incident
|
|
1068
|
+
method: POST # subEntityAdd pattern — AddIncidentCommand generado
|
|
1069
|
+
path: /{id}/incidents # relativo al basePath
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
El nombre del useCase debe contener palabras que tracen semánticamente con **todos** los valores del `*Type` que ese endpoint puede crear. Si distintos valores tienen orígenes distintos (algunos por HTTP, otros por Kafka), trazar cada uno por su mecanismo correspondiente.
|
|
1073
|
+
|
|
1074
|
+
> **Regla de oro:** para cada valor de `FooType`, al menos uno de los listeners, endpoints o domain events del módulo debe contener una palabra semántica equivalente. Si ninguno la contiene → falta diseño.
|
|
1075
|
+
|
|
1076
|
+
### Checklist del domain.yaml por módulo
|
|
1077
|
+
|
|
1078
|
+
Antes de guardar `system/{module}.yaml`, verifica:
|
|
1079
|
+
|
|
1080
|
+
- [ ] Campo `id` presente en todas las entidades
|
|
1081
|
+
- [ ] Solo una entidad con `isRoot: true` por agregado
|
|
1082
|
+
- [ ] Relaciones intra-agregado declaradas solo en la entidad raíz; lado inverso NO declarado en la secundaria
|
|
1083
|
+
- [ ] Campos de auditoría NO en `fields:` — los genera el framework
|
|
1084
|
+
- [ ] Campos `readOnly` con `defaultValue` si tienen valor inicial conocido
|
|
1085
|
+
- [ ] Enums con ciclo de vida tienen `initialValue` y `transitions`
|
|
1086
|
+
- [ ] Value Objects sin campo `id`
|
|
1087
|
+
- [ ] Referencias cross-aggregate usando `reference:` en el campo ID, no `relationships:`
|
|
1088
|
+
- [ ] `events[].name` consistente con `integrations.async[]` donde `producer` es este módulo
|
|
1089
|
+
- [ ] `events[].topic` declarado en cada evento — debe coincidir **exactamente** con `integrations.async[].topic`; si se omite, el generador lo deriva quitando el sufijo `Event` (`OrderPlacedEvent` → `ORDER_PLACED`), pero declararlo explícitamente evita mismatch con consumidores
|
|
1090
|
+
- [ ] `events[].triggers[]` referencia métodos que existen en `enums[].transitions[].method`
|
|
1091
|
+
- [ ] `{entityName}Id` declarado en `events[].fields` cuando el evento **cruza módulos via Kafka** (el generador lo mapea a `event.getAggregateId()` automáticamente)
|
|
1092
|
+
- [ ] `endpoints[].operations[].useCase` consistente con `modules[].exposes[].useCase`
|
|
1093
|
+
- [ ] `endpoints:` tiene la estructura `{ basePath, versions: [{ version, operations }] }` — **no** es una lista plana de operaciones
|
|
1094
|
+
- [ ] `endpoints[].path` son **relativos** al basePath
|
|
1095
|
+
- [ ] `listeners[]` presentes para todos los eventos donde este módulo es consumidor
|
|
1096
|
+
- [ ] `listeners[].useCase` coincide con `consumers[].useCase` en `system/system.yaml`
|
|
1097
|
+
- [ ] `listeners[].topic` coincide **exactamente** con `integrations.async[].topic` en `system/system.yaml` — valor bare `SCREAMING_SNAKE_CASE`, **nunca** con `topicPrefix` prepended (ej: `PAYMENT_APPROVED`, no `test-eva.PAYMENT_APPROVED`)
|
|
1098
|
+
- [ ] `ports[]` presentes para todas las entradas `integrations.sync[]` donde `caller = este módulo`
|
|
1099
|
+
- [ ] `ports[].baseUrl` solo en la primera entrada de cada `service:`
|
|
1100
|
+
- [ ] `listeners[].nestedTypes[]` declarado para cada campo de tipo objeto en `fields[]`
|
|
1101
|
+
- [ ] Cada transición activada por respuesta de `ports:` (éxito o error) tiene un domain event con `triggers` correspondiente
|
|
1102
|
+
- [ ] Cada transición activada por scheduler o proceso interno tiene un domain event con `triggers` correspondiente
|
|
1103
|
+
- [ ] Para cada enum `*Type`: cada valor tiene trazabilidad semántica en un listener (nombre de evento o campo) o en un endpoint (useCase) del módulo
|
|
1104
|
+
- [ ] Para entidades secundarias en `OneToMany` con campo `type: *Type` creado por HTTP: existe endpoint `RegisterX` / `AddX` con useCase que traza semánticamente con los valores del enum
|
|
1105
|
+
- [ ] Cada campo `readOnly` sin `defaultValue` y sin tipo de estado (enum con `transitions`): la transición que lo asigna tiene un evento con `triggers` que lo incluye en `fields[]`
|
|
1106
|
+
- [ ] Proyecciones locales (agregados sincronizados por listeners): `audit.enabled: true` y `trackUser: false`
|
|
1107
|
+
- [ ] Todo el contenido está en **inglés**
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
## Paso 8 — Crear archivos individuales por módulo (`system/{module}.md`)
|
|
1111
|
+
|
|
1112
|
+
Inmediatamente después de crear los archivos `system/{module}.yaml`, genera **un archivo `.md` separado por cada módulo** declarado en `modules:`. La ruta es `system/{nombre-del-modulo}.md` (mismo directorio `system/`).
|
|
1113
|
+
|
|
1114
|
+
El objetivo es tener una **especificación técnica desagregada** que un desarrollador pueda leer de forma independiente para implementar su módulo sin necesidad de conocer el sistema completo.
|
|
1115
|
+
|
|
1116
|
+
### Estructura obligatoria de cada `system/{module}.md`
|
|
1117
|
+
|
|
1118
|
+
```markdown
|
|
1119
|
+
# {nombre-del-modulo} — Especificación técnica
|
|
1120
|
+
|
|
1121
|
+
## Rol del módulo
|
|
1122
|
+
[3–5 párrafos MUY DETALLADOS]
|
|
1123
|
+
- Qué problema de negocio resuelve y qué entidades/conceptos gestiona
|
|
1124
|
+
- Cuáles son sus responsabilidades exclusivas (límites del bounded context)
|
|
1125
|
+
- Qué NO es responsabilidad de este módulo
|
|
1126
|
+
- Cómo colabora con los otros módulos del sistema
|
|
1127
|
+
|
|
1128
|
+
## Invariantes
|
|
1129
|
+
|
|
1130
|
+
> Las invariantes son condiciones que deben ser **siempre verdaderas** dentro de este bounded context,
|
|
1131
|
+
> independientemente de qué operación se ejecute. Violar una invariante es un **error de negocio crítico**
|
|
1132
|
+
> que debe lanzar excepción y nunca persistirse.
|
|
1133
|
+
|
|
1134
|
+
| ID | Invariante | Consecuencia de violación |
|
|
1135
|
+
|----|-----------|---------------------------|
|
|
1136
|
+
| INV-01 | [condición que siempre debe cumplirse] | [qué excepción lanzar / qué impide] |
|
|
1137
|
+
| INV-02 | ... | ... |
|
|
1138
|
+
|
|
1139
|
+
> **Regla de implementación:** cada caso de uso debe verificar las invariantes relevantes **antes** de persistir cualquier cambio. Preferir verificación en el método de negocio de la entidad de dominio.
|
|
1140
|
+
|
|
1141
|
+
## Máquina de estados
|
|
1142
|
+
[SOLO si el módulo gestiona un ciclo de vida — omitir la sección si no aplica]
|
|
1143
|
+
|
|
1144
|
+
```mermaid
|
|
1145
|
+
stateDiagram-v2
|
|
1146
|
+
[*] --> ESTADO_INICIAL
|
|
1147
|
+
ESTADO_INICIAL --> ESTADO_SIGUIENTE : evento / acción
|
|
1148
|
+
ESTADO_SIGUIENTE --> ESTADO_FINAL : evento / acción
|
|
1149
|
+
ESTADO_FINAL --> [*]
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
> Restricciones de transición: [explicar qué transiciones están prohibidas y por qué — estas son invariantes implícitas de la máquina de estados]
|
|
1153
|
+
|
|
1154
|
+
## Diagrama de interacciones
|
|
1155
|
+
|
|
1156
|
+
> Muestra el flujo completo: qué llega al módulo (endpoint HTTP o evento asíncrono), qué caso de uso se ejecuta y qué evento se emite como resultado.
|
|
1157
|
+
|
|
1158
|
+
```mermaid
|
|
1159
|
+
flowchart TD
|
|
1160
|
+
subgraph HTTP["Endpoints REST"]
|
|
1161
|
+
EP1["METHOD /path"]
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
subgraph ASYNC_IN["Eventos entrantes"]
|
|
1165
|
+
EI1["📥 NombreDelEventoEvent"]
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
subgraph USE_CASES["Casos de uso"]
|
|
1169
|
+
UC1["NombreDelUseCase"]
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
subgraph EVENTS_OUT["Eventos emitidos"]
|
|
1173
|
+
EO1["📤 NombreDelEventoEmitidoEvent"]
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
EP1 --> UC1
|
|
1177
|
+
EI1 --> UC2["OtroUseCase"]
|
|
1178
|
+
UC1 --> EO1
|
|
1179
|
+
UC2 -->|"sin evento"| FIN([fin])
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
> **Convención de colores / nodos:**
|
|
1183
|
+
> - Endpoints HTTP → nodos rectangulares con el método y path
|
|
1184
|
+
> - Eventos entrantes → nodos con prefijo `📥`
|
|
1185
|
+
> - Casos de uso → nodos rectangulares con el nombre del handler
|
|
1186
|
+
> - Eventos emitidos → nodos con prefijo `📤`
|
|
1187
|
+
> - Flujos sin evento de salida → conectar al nodo `FIN([fin])`
|
|
1188
|
+
> - Omitir subgraph `ASYNC_IN` si el módulo no consume eventos
|
|
1189
|
+
> - Omitir subgraph `EVENTS_OUT` si el módulo no emite eventos
|
|
1190
|
+
|
|
1191
|
+
## Diagrama de secuencia
|
|
1192
|
+
[Muestra las interacciones cronológicas entre actores y componentes para los flujos principales. Generar un diagrama por cada caso de uso complejo o flujo con bifurcaciones significativas (happy path + rama de error/compensación).]
|
|
1193
|
+
|
|
1194
|
+
> **Rol de experto en diagramas de sistemas:** modela cada actor involucrado — usuarios externos, sistemas externos, módulos del dominio, broker de mensajes, base de datos — y sus interacciones en orden temporal. Priorizar claridad: mostrar qué mensaje se envía, quién responde y en qué orden. Las respuestas asíncronas (eventos Kafka) deben modelarse con líneas punteadas.
|
|
1195
|
+
|
|
1196
|
+
```mermaid
|
|
1197
|
+
sequenceDiagram
|
|
1198
|
+
actor Client as Client
|
|
1199
|
+
participant API as REST API
|
|
1200
|
+
participant Handler as CommandHandler
|
|
1201
|
+
participant Domain as Domain Entity
|
|
1202
|
+
participant Repo as Repository
|
|
1203
|
+
participant Broker as Message Broker
|
|
1204
|
+
participant ExtSvc as External Service
|
|
1205
|
+
|
|
1206
|
+
Client->>API: POST /resource {payload}
|
|
1207
|
+
API->>Handler: CreateResourceCommand
|
|
1208
|
+
Handler->>Repo: findById(id)
|
|
1209
|
+
Repo-->>Handler: entity (or 404)
|
|
1210
|
+
Handler->>Domain: businessMethod()
|
|
1211
|
+
Domain-->>Handler: updated entity
|
|
1212
|
+
Handler->>Repo: save(entity)
|
|
1213
|
+
Handler->>Broker: publish(ResourceCreatedEvent)
|
|
1214
|
+
Broker-->>Handler: ack
|
|
1215
|
+
Handler-->>API: resourceId
|
|
1216
|
+
API-->>Client: 201 Created {resourceId}
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
> **Convención de actores en el diagrama de secuencia:**
|
|
1220
|
+
> - `actor Client` → usuarios humanos o sistemas externos que inician el flujo
|
|
1221
|
+
> - `participant API` → controlador REST del módulo
|
|
1222
|
+
> - `participant Handler` → CommandHandler / QueryHandler de la capa de aplicación
|
|
1223
|
+
> - `participant Domain` → entidad de dominio (incluir cuando la lógica de negocio es relevante para entender el flujo)
|
|
1224
|
+
> - `participant Repo` → repositorio (abstracción de persistencia)
|
|
1225
|
+
> - `participant Broker` → broker de mensajes — omitir si el módulo no emite eventos
|
|
1226
|
+
> - `participant ExtSvc` → servicio externo via `ports:` — omitir si no aplica; usar el nombre real del port (ej: `PaymentGateway`)
|
|
1227
|
+
> - Respuestas síncronas: `-->>` (línea punteada con flecha)
|
|
1228
|
+
> - Mensajes async / eventos publicados: `--)` (línea punteada sin flecha de retorno inmediato)
|
|
1229
|
+
> - Incluir un diagrama separado por cada flujo principal; omitir flujos triviales de solo lectura si no aportan información nueva
|
|
1230
|
+
|
|
1231
|
+
## Casos de uso
|
|
1232
|
+
[Un apartado `###` por cada useCase declarado en `exposes:` y en `consumers[].useCase` donde este módulo es consumidor]
|
|
1233
|
+
|
|
1234
|
+
### {NombreDelUseCase}
|
|
1235
|
+
**Tipo:** `HTTP` | `Evento entrante` (indicar cuál)
|
|
1236
|
+
**Qué hace:** [descripción detallada de la lógica de negocio]
|
|
1237
|
+
**Precondiciones:** [estados válidos, entidades que deben existir]
|
|
1238
|
+
**Postcondiciones:** [estado del sistema tras ejecución exitosa]
|
|
1239
|
+
**Invariantes verificadas:** [lista de IDs — ej: INV-01, INV-03]
|
|
1240
|
+
**Validaciones y errores:** [condiciones que lanzan excepción, tipo de error y HTTP status]
|
|
1241
|
+
**Eventos que emite:** [nombre del DomainEvent y condición, o "ninguno"]
|
|
1242
|
+
|
|
1243
|
+
**Diagrama de flujo:**
|
|
1244
|
+
```mermaid
|
|
1245
|
+
flowchart TD
|
|
1246
|
+
IN["🔵 Trigger: METHOD /path · or EventName"] --> UC["⚙️ {NombreDelUseCase}"]
|
|
1247
|
+
UC --> INV{"Check invariants"}
|
|
1248
|
+
INV -->|"violated"| ERR[/"Exception / Error Response"/]
|
|
1249
|
+
INV -->|"passed"| BIZ["Execute business logic"]
|
|
1250
|
+
BIZ --> SAVE["Persist changes"]
|
|
1251
|
+
SAVE -->|"if applicable"| EV["📤 DomainEvent emitted"]
|
|
1252
|
+
SAVE -->|"no event"| FIN([end])
|
|
1253
|
+
```
|
|
1254
|
+
> Adaptar al flujo real: incluir nodos y bifurcaciones de negocio relevantes; omitir los que no participan en este caso de uso específico.
|
|
1255
|
+
|
|
1256
|
+
## Endpoints expuestos
|
|
1257
|
+
[Solo si el módulo tiene entradas en `exposes:`]
|
|
1258
|
+
|
|
1259
|
+
### {METHOD} {/path}
|
|
1260
|
+
**Caso de uso:** `{UseCase}`
|
|
1261
|
+
**Propósito:** [descripción del endpoint]
|
|
1262
|
+
**Path params / Query params:** [descripción de cada parámetro]
|
|
1263
|
+
**Request body:** [campos esperados, tipos, restricciones de validación]
|
|
1264
|
+
**Response:** [campos devueltos y su significado de negocio]
|
|
1265
|
+
**Errores:** [HTTP status codes posibles y cuándo ocurren]
|
|
1266
|
+
|
|
1267
|
+
## Eventos emitidos
|
|
1268
|
+
[Solo si el módulo es producer en `integrations.async`]
|
|
1269
|
+
|
|
1270
|
+
### {NombreDelEvento}
|
|
1271
|
+
**Cuándo:** [condición exacta de negocio que dispara el evento]
|
|
1272
|
+
**Payload:** [campos del evento con descripción de cada uno]
|
|
1273
|
+
**Consumidores y sus acciones:**
|
|
1274
|
+
- `{modulo}` → `{useCase}`: [qué hace el consumidor al recibirlo]
|
|
1275
|
+
|
|
1276
|
+
## Puertos (llamadas síncronas salientes)
|
|
1277
|
+
[Solo si el módulo aparece como `caller` en `integrations.sync`]
|
|
1278
|
+
|
|
1279
|
+
### {NombreDelPort} → {modulo-destino}
|
|
1280
|
+
**Cuándo se llama:** [en qué caso de uso y bajo qué condición]
|
|
1281
|
+
**Endpoints usados:** [lista de METHOD /path]
|
|
1282
|
+
**Datos que obtiene y cómo los usa:** [descripción detallada]
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
### Reglas de los archivos de módulo
|
|
1286
|
+
|
|
1287
|
+
- **Todo el contenido en inglés**: títulos, secciones, descripciones, invariantes, casos de uso — siempre en inglés.
|
|
1288
|
+
- **Las INVARIANTES son obligatorias**: todo módulo debe tener al menos 2–3 invariantes. Si no se te ocurren, analiza las reglas implícitas del negocio (unicidad, estados válidos, rangos, precondiciones de transición).
|
|
1289
|
+
- **El diagrama de interacciones (flowchart) es obligatorio**: debe incluir todos los endpoints y eventos entrantes del módulo. Si el módulo no tiene entradas HTTP ni eventos, usar un nodo `[[módulo pasivo]]`.
|
|
1290
|
+
- **El diagrama de secuencia es obligatorio**: generar al menos un diagrama `sequenceDiagram` cubriendo el flujo principal (happy path) del módulo. Agregar diagramas adicionales para flujos con bifurcaciones significativas (error, compensación, async). Actuar como experto en diagramas de sistemas: modelar todos los actores reales (usuarios, API, handlers, dominio, repositorio, broker, servicios externos).
|
|
1291
|
+
- **Cada caso de uso debe incluir su propio diagrama de flujo**: generar un `flowchart TD` dentro de cada sección `### {UseCase}` mostrando el trigger de entrada (endpoint HTTP o evento), las verificaciones de invariantes, la lógica de negocio y los eventos emitidos. Adaptar al flujo específico; omitir nodos que no participan.
|
|
1292
|
+
- **La máquina de estados es condicional**: incluirla solo si el módulo gestiona entidades con ciclo de vida (estados). Las restricciones de transición son invariantes implícitas y deben listarse también en la tabla de invariantes.
|
|
1293
|
+
- **Cada caso de uso debe referenciar las invariantes**: usar los IDs de la tabla (INV-01, INV-02…) en el campo `Invariants verified`.
|
|
1294
|
+
- **No duplicar**: el contenido de `system.md` es un resumen; el archivo de módulo es la especificación completa. No copiar exactamente el mismo texto.
|
|
1295
|
+
- Los archivos se guardan en `system/{nombre-del-modulo}.md` — mismo directorio que `system.yaml`.
|
|
1296
|
+
|
|
1297
|
+
### Ejemplo condensado — `system/payments.md`
|
|
1298
|
+
|
|
1299
|
+
```markdown
|
|
1300
|
+
# payments — Especificación técnica
|
|
1301
|
+
|
|
1302
|
+
## Rol del módulo
|
|
1303
|
+
|
|
1304
|
+
El módulo `payments` es el único responsable del ciclo de vida de los pagos vinculados
|
|
1305
|
+
a reservas. Gestiona la comunicación con la pasarela de pago externa, registra el estado
|
|
1306
|
+
de cada transacción y decide cuándo emitir los eventos que desencadenan cambios en otros
|
|
1307
|
+
módulos. No conoce la lógica interna de reservas ni de notificaciones.
|
|
1308
|
+
|
|
1309
|
+
No es responsabilidad de este módulo confirmar reservas ni enviar notificaciones;
|
|
1310
|
+
esos flujos son iniciados por los eventos que payments emite tras cada transacción.
|
|
1311
|
+
|
|
1312
|
+
## Invariantes
|
|
1313
|
+
|
|
1314
|
+
| ID | Invariante | Consecuencia de violación |
|
|
1315
|
+
|----|-----------|---------------------------|
|
|
1316
|
+
| INV-01 | Solo puede existir un pago activo (PENDING o APPROVED) por reserva en cualquier momento | Lanza `409 Conflict` — impide doble cobro |
|
|
1317
|
+
| INV-02 | Un pago CANCELLED o FAILED no puede pasar a APPROVED | Lanza `InvalidStateTransitionException` |
|
|
1318
|
+
| INV-03 | El monto del pago debe ser > 0 | Lanza `400 Bad Request` — ninguna operación de cobro puede procesarse con monto cero o negativo |
|
|
1319
|
+
| INV-04 | Un pago solo puede reembolsarse si está en estado APPROVED | Lanza `409 Conflict` — no se puede reembolsar lo que no fue cobrado |
|
|
1320
|
+
|
|
1321
|
+
## Máquina de estados
|
|
1322
|
+
|
|
1323
|
+
```mermaid
|
|
1324
|
+
stateDiagram-v2
|
|
1325
|
+
[*] --> PENDING : CreatePayment
|
|
1326
|
+
PENDING --> APPROVED : ApprovePayment (pasarela confirma)
|
|
1327
|
+
PENDING --> FAILED : FailPayment (pasarela rechaza)
|
|
1328
|
+
APPROVED --> REFUNDED : RefundPayment
|
|
1329
|
+
FAILED --> [*]
|
|
1330
|
+
REFUNDED --> [*]
|
|
1331
|
+
```
|
|
1332
|
+
|
|
1333
|
+
> Restricciones: CANCELLED y FAILED son estados terminales (INV-02). Solo APPROVED puede transicionar a REFUNDED (INV-04).
|
|
1334
|
+
|
|
1335
|
+
## Diagrama de interacciones
|
|
1336
|
+
|
|
1337
|
+
```mermaid
|
|
1338
|
+
flowchart TD
|
|
1339
|
+
subgraph HTTP["Endpoints REST"]
|
|
1340
|
+
EP1["POST /payments"]
|
|
1341
|
+
EP2["GET /payments/{id}"]
|
|
1342
|
+
EP3["POST /payments/{id}/refund"]
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1345
|
+
subgraph ASYNC_IN["Eventos entrantes"]
|
|
1346
|
+
EI1["📥 ReservationCreatedEvent"]
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
subgraph USE_CASES["Casos de uso"]
|
|
1350
|
+
UC1["CreatePayment"]
|
|
1351
|
+
UC2["GetPayment"]
|
|
1352
|
+
UC3["RefundPayment"]
|
|
1353
|
+
UC4["HandleReservationCreated"]
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1356
|
+
subgraph EVENTS_OUT["Eventos emitidos"]
|
|
1357
|
+
EO1["📤 PaymentApprovedEvent"]
|
|
1358
|
+
EO2["📤 PaymentFailedEvent"]
|
|
1359
|
+
EO3["📤 PaymentRefundedEvent"]
|
|
1360
|
+
end
|
|
1361
|
+
|
|
1362
|
+
EP1 --> UC1
|
|
1363
|
+
EP2 --> UC2
|
|
1364
|
+
EP3 --> UC3
|
|
1365
|
+
EI1 --> UC4
|
|
1366
|
+
UC4 --> UC1
|
|
1367
|
+
UC1 --> EO1
|
|
1368
|
+
UC1 --> EO2
|
|
1369
|
+
UC3 --> EO3
|
|
1370
|
+
UC2 -->|"solo lectura"| FIN([fin])
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
## Diagrama de secuencia
|
|
1374
|
+
|
|
1375
|
+
```mermaid
|
|
1376
|
+
sequenceDiagram
|
|
1377
|
+
actor Client as Client
|
|
1378
|
+
participant API as REST API (PaymentController)
|
|
1379
|
+
participant Handler as CreatePaymentHandler
|
|
1380
|
+
participant Domain as Payment (Domain Entity)
|
|
1381
|
+
participant Repo as PaymentRepository
|
|
1382
|
+
participant GW as PaymentGateway (port)
|
|
1383
|
+
participant Broker as Kafka
|
|
1384
|
+
|
|
1385
|
+
Note over Client,Broker: Flow: CreatePayment (triggered by ReservationCreatedEvent or direct HTTP)
|
|
1386
|
+
|
|
1387
|
+
Client->>API: POST /payments {reservationId, paymentMethod}
|
|
1388
|
+
API->>Handler: CreatePaymentCommand
|
|
1389
|
+
Handler->>Repo: findActiveByReservationId(reservationId)
|
|
1390
|
+
Repo-->>Handler: empty (INV-01 check passed)
|
|
1391
|
+
Handler->>GW: processPayment({amount, method})
|
|
1392
|
+
GW-->>Handler: {paymentId, status: APPROVED}
|
|
1393
|
+
Handler->>Domain: new Payment(...) + approve()
|
|
1394
|
+
Domain-->>Handler: Payment [APPROVED]
|
|
1395
|
+
Handler->>Repo: save(payment)
|
|
1396
|
+
Handler->>Broker: publish(PaymentApprovedEvent)
|
|
1397
|
+
Broker--)API: ack
|
|
1398
|
+
Handler-->>API: paymentId
|
|
1399
|
+
API-->>Client: 201 Created {paymentId, status}
|
|
1400
|
+
|
|
1401
|
+
Note over Client,Broker: Failure branch: gateway rejects the charge
|
|
1402
|
+
GW-->>Handler: error / rejection
|
|
1403
|
+
Handler->>Domain: new Payment(...) + fail()
|
|
1404
|
+
Domain-->>Handler: Payment [FAILED]
|
|
1405
|
+
Handler->>Repo: save(payment)
|
|
1406
|
+
Handler->>Broker: publish(PaymentFailedEvent)
|
|
1407
|
+
Handler-->>API: 422 Unprocessable Entity
|
|
1408
|
+
API-->>Client: 422 {reason}
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
## Casos de uso
|
|
1412
|
+
|
|
1413
|
+
### HandleReservationCreated
|
|
1414
|
+
**Tipo:** Evento entrante (`ReservationCreatedEvent`)
|
|
1415
|
+
**Qué hace:** Extrae `reservationId` y `totalAmount` del payload y dispara `CreatePayment`
|
|
1416
|
+
internamente para iniciar el cobro de forma automática.
|
|
1417
|
+
**Precondiciones:** Payload contiene `reservationId` válido y `totalAmount > 0`.
|
|
1418
|
+
**Postcondiciones:** Pago creado en estado PENDING.
|
|
1419
|
+
**Invariantes verificadas:** INV-01, INV-03
|
|
1420
|
+
**Validaciones y errores:** Si INV-01 se viola (ya existe pago activo), descarta el evento
|
|
1421
|
+
y registra warning. Si `totalAmount ≤ 0`, lanza `400`.
|
|
1422
|
+
**Eventos que emite:** ninguno directamente; la aprobación llega asíncronamente.
|
|
1423
|
+
|
|
1424
|
+
**Diagrama de flujo:**
|
|
1425
|
+
```mermaid
|
|
1426
|
+
flowchart TD
|
|
1427
|
+
IN["📥 ReservationCreatedEvent"] --> UC["⚙️ HandleReservationCreated"]
|
|
1428
|
+
UC --> INV1{"INV-01: active payment for reservation?"}
|
|
1429
|
+
INV1 -->|"Yes"| WARN[/"warn — discard event"/]
|
|
1430
|
+
INV1 -->|"No"| INV2{"INV-03: amount > 0?"}
|
|
1431
|
+
INV2 -->|"No"| ERR[/"400 Bad Request"/]
|
|
1432
|
+
INV2 -->|"Yes"| CREATE["Create Payment PENDING"]
|
|
1433
|
+
CREATE --> SAVE["Persist Payment"]
|
|
1434
|
+
SAVE --> FIN([end — approval arrives async])
|
|
1435
|
+
```
|
|
1436
|
+
```
|
|
1437
|
+
|
|
1438
|
+
---
|
|
1439
|
+
|
|
1440
|
+
## Ciclo de refinamiento
|
|
1441
|
+
|
|
1442
|
+
Después de entregar el `system.yaml` v1, el usuario puede pedir ajustes:
|
|
1443
|
+
- Aplica el **cambio mínimo** necesario (no rehaces todo el archivo)
|
|
1444
|
+
- Vuelve a validar el checklist del Paso 4
|
|
1445
|
+
- Actualiza `system/system.md`, `system/system.mmd`, `system/{module}.yaml` **y** `system/{module}.md` del módulo afectado
|
|
1446
|
+
- Entrega solo el diff explicado, no los archivos completos de nuevo (salvo que se pida)
|