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.
Files changed (106) hide show
  1. package/AGENTS.md +314 -10
  2. package/COMMAND_EVALUATION.md +15 -16
  3. package/DOMAIN_YAML_GUIDE.md +576 -10
  4. package/FUTURE_FEATURES.md +1627 -1168
  5. package/README.md +318 -13
  6. package/bin/eva4j.js +34 -0
  7. package/config/defaults.json +1 -0
  8. package/design-system.md +797 -0
  9. package/docs/commands/EVALUATE_SYSTEM.md +994 -0
  10. package/docs/commands/GENERATE_ENTITIES.md +795 -6
  11. package/docs/commands/INDEX.md +10 -1
  12. package/examples/domain-endpoints-relations.yaml +353 -0
  13. package/examples/domain-endpoints-versioned.yaml +144 -0
  14. package/examples/domain-endpoints.yaml +135 -0
  15. package/examples/domain-events.yaml +166 -20
  16. package/examples/domain-listeners.yaml +212 -0
  17. package/examples/domain-one-to-many.yaml +1 -0
  18. package/examples/domain-one-to-one.yaml +1 -0
  19. package/examples/domain-ports.yaml +414 -0
  20. package/examples/domain-soft-delete.yaml +47 -44
  21. package/examples/system/notification.yaml +147 -0
  22. package/examples/system/product.yaml +185 -0
  23. package/examples/system/system.yaml +112 -0
  24. package/examples/system-report.html +971 -0
  25. package/examples/system.yaml +332 -0
  26. package/package.json +2 -1
  27. package/src/commands/build.js +714 -0
  28. package/src/commands/create.js +7 -3
  29. package/src/commands/detach.js +1 -0
  30. package/src/commands/evaluate-system.js +610 -0
  31. package/src/commands/generate-entities.js +1331 -49
  32. package/src/commands/generate-http-exchange.js +2 -0
  33. package/src/commands/generate-kafka-event.js +98 -11
  34. package/src/generators/base-generator.js +8 -1
  35. package/src/generators/postman-generator.js +188 -0
  36. package/src/generators/shared-generator.js +10 -0
  37. package/src/utils/config-manager.js +54 -0
  38. package/src/utils/context-builder.js +1 -0
  39. package/src/utils/domain-diagram.js +192 -0
  40. package/src/utils/domain-validator.js +970 -0
  41. package/src/utils/fake-data.js +376 -0
  42. package/src/utils/naming.js +3 -2
  43. package/src/utils/system-validator.js +434 -0
  44. package/src/utils/yaml-to-entity.js +302 -8
  45. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  46. package/templates/aggregate/AggregateRepository.java.ejs +8 -2
  47. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
  48. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  49. package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
  50. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  51. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  52. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  53. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  54. package/templates/base/gradle/build.gradle.ejs +3 -2
  55. package/templates/base/root/AGENTS.md.ejs +306 -45
  56. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
  57. package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
  58. package/templates/base/root/system.yaml.ejs +97 -0
  59. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  60. package/templates/crud/Controller.java.ejs +4 -4
  61. package/templates/crud/CreateCommand.java.ejs +4 -0
  62. package/templates/crud/CreateItemDto.java.ejs +4 -0
  63. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  64. package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
  65. package/templates/crud/EndpointsController.java.ejs +178 -0
  66. package/templates/crud/FindByQuery.java.ejs +17 -0
  67. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  68. package/templates/crud/ListQuery.java.ejs +1 -1
  69. package/templates/crud/ListQueryHandler.java.ejs +8 -8
  70. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  71. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  72. package/templates/crud/ScaffoldQuery.java.ejs +13 -0
  73. package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
  74. package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
  75. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  76. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  77. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  78. package/templates/crud/TransitionCommand.java.ejs +9 -0
  79. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  80. package/templates/crud/UpdateCommand.java.ejs +4 -0
  81. package/templates/evaluate/report.html.ejs +1363 -0
  82. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  83. package/templates/kafka-event/Event.java.ejs +16 -0
  84. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  85. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  86. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  87. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  88. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
  89. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  90. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  91. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  92. package/templates/mock/MockEvent.java.ejs +10 -0
  93. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  94. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  95. package/templates/mock/SpringEventListener.java.ejs +61 -0
  96. package/templates/ports/PortDomainModel.java.ejs +35 -0
  97. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  98. package/templates/ports/PortFeignClient.java.ejs +45 -0
  99. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  100. package/templates/ports/PortInterface.java.ejs +45 -0
  101. package/templates/ports/PortNestedType.java.ejs +28 -0
  102. package/templates/ports/PortRequestDto.java.ejs +30 -0
  103. package/templates/ports/PortResponseDto.java.ejs +28 -0
  104. package/templates/postman/Collection.json.ejs +1 -1
  105. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  106. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
@@ -1,1168 +1,1627 @@
1
- # Características Futuras - eva4j
2
-
3
- Este documento describe las mejoras planificadas para futuras versiones de eva4j, organizadas por prioridad. Cada sección incluye el contexto DDD correspondiente, la sintaxis YAML propuesta y ejemplos del código que se generaría.
4
-
5
- ---
6
-
7
- ## � Tabla de Contenidos
8
-
9
- ### � Alta Prioridad
10
- - [Domain Events](#1-domain-events)
11
- - [Aggregate Boundaries por ID](#2-aggregate-boundaries-por-id)
12
- - [Soft Delete Completo](#3-soft-delete-completo)
13
-
14
- ### � Media Prioridad
15
- - [Paginación en Queries](#4-paginación-en-queries) ✅
16
- - [Optimistic Locking](#5-optimistic-locking)
17
- - [Read Models Separados](#6-read-models-separados-proyecciones)
18
- - [Enums con Comportamiento y Transiciones](#7-enums-con-comportamiento-y-transiciones)
19
- - [Políticas y Especificaciones](#8-políticas-y-especificaciones)
20
-
21
- ### � Tooling y Calidad
22
- - [Validación de domain.yaml con JSON Schema](#9-validación-de-domainyaml-con-json-schema)
23
- - [Generación Incremental / Diff](#10-generación-incremental--diff)
24
- - [Comando eva4j doctor](#11-comando-eva4j-doctor)
25
- - [Tests Generados Completos](#12-tests-generados-completos)
26
-
27
- ### Implementado
28
- - [Auditoría de Tiempo y Usuario](#13-auditoría-implementada)
29
- - [Validaciones JSR-303](#14-validaciones-jsr-303-implementado)
30
- - [Enums con Comportamiento y Transiciones](#7-enums-con-comportamiento-y-transiciones)
31
- - [Generación Incremental / Diff](#10-generación-incremental--diff)
32
- - [Paginación en Queries](#4-paginación-en-queries)
33
- - [Aggregate Boundaries por ID](#2-aggregate-boundaries-por-id-implementado)
34
-
35
- ---
36
-
37
- ## ALTA PRIORIDAD
38
-
39
- ---
40
-
41
- ## 1. Domain Events
42
-
43
- ### Descripción
44
-
45
- Los **Domain Events** son el patrón más fundamental de DDD que actualmente falta en eva4j. Un evento de dominio representa algo significativo que ocurrió en el negocio — un hecho pasado, no una intención futura. Son esenciales para:
46
-
47
- - Comunicar cambios entre agregados sin acoplamiento directo
48
- - Disparar side effects (emails, notificaciones, actualizaciones de proyecciones)
49
- - Construir sistemas eventualmente consistentes
50
-
51
- Sin eventos de dominio, la comunicación entre agregados obliga a dependencias directas que violan los límites de los bounded contexts.
52
-
53
- ### Sintaxis Propuesta en domain.yaml
54
-
55
- ```yaml
56
- aggregates:
57
- - name: Order
58
- entities:
59
- - name: order
60
- isRoot: true
61
- events:
62
- - name: OrderPlaced
63
- fields:
64
- - name: orderId
65
- type: String
66
- - name: customerId
67
- type: String
68
- - name: totalAmount
69
- type: BigDecimal
70
- - name: OrderCancelled
71
- fields:
72
- - name: orderId
73
- type: String
74
- - name: reason
75
- type: String
76
- ```
77
-
78
- ### Código Generado
79
-
80
- #### Clase base DomainEvent
81
-
82
- ```java
83
- // shared/domain/DomainEvent.java
84
- public abstract class DomainEvent {
85
- private final String eventId;
86
- private final LocalDateTime occurredOn;
87
- private final String aggregateId;
88
-
89
- protected DomainEvent(String aggregateId) {
90
- this.eventId = UUID.randomUUID().toString();
91
- this.occurredOn = LocalDateTime.now();
92
- this.aggregateId = aggregateId;
93
- }
94
-
95
- public String getEventId() { return eventId; }
96
- public LocalDateTime getOccurredOn() { return occurredOn; }
97
- public String getAggregateId() { return aggregateId; }
98
- }
99
- ```
100
-
101
- #### Evento específico generado
102
-
103
- ```java
104
- // domain/models/events/OrderPlacedEvent.java
105
- public class OrderPlacedEvent extends DomainEvent {
106
- private final String customerId;
107
- private final BigDecimal totalAmount;
108
-
109
- public OrderPlacedEvent(String orderId, String customerId, BigDecimal totalAmount) {
110
- super(orderId);
111
- this.customerId = customerId;
112
- this.totalAmount = totalAmount;
113
- }
114
-
115
- public String getCustomerId() { return customerId; }
116
- public BigDecimal getTotalAmount() { return totalAmount; }
117
- }
118
- ```
119
-
120
- #### Raíz del agregado con eventos
121
-
122
- ```java
123
- // domain/models/entities/Order.java
124
- public class Order {
125
- private List<DomainEvent> domainEvents = new ArrayList<>();
126
-
127
- public List<DomainEvent> getDomainEvents() {
128
- return Collections.unmodifiableList(domainEvents);
129
- }
130
-
131
- public void clearDomainEvents() {
132
- domainEvents.clear();
133
- }
134
-
135
- public void place(String customerId, BigDecimal total) {
136
- this.status = OrderStatus.PLACED;
137
- domainEvents.add(new OrderPlacedEvent(this.id, customerId, total));
138
- }
139
-
140
- public void cancel(String reason) {
141
- if (this.status == OrderStatus.DELIVERED) {
142
- throw new IllegalStateException("Cannot cancel a delivered order");
143
- }
144
- this.status = OrderStatus.CANCELLED;
145
- domainEvents.add(new OrderCancelledEvent(this.id, reason));
146
- }
147
- }
148
- ```
149
-
150
- #### Publicación automática desde el repositorio
151
-
152
- ```java
153
- @Override
154
- public Order save(Order order) {
155
- OrderJpa jpa = mapper.toJpa(order);
156
- repository.save(jpa);
157
- order.getDomainEvents().forEach(eventPublisher::publishEvent);
158
- order.clearDomainEvents();
159
- return mapper.toDomain(jpa);
160
- }
161
- ```
162
-
163
- #### Listener en otro módulo (sin acoplamiento)
164
-
165
- ```java
166
- @Component
167
- public class OrderEventListener {
168
- @EventListener
169
- public void onOrderPlaced(OrderPlacedEvent event) {
170
- // enviar email de confirmación, actualizar inventario, etc.
171
- }
172
-
173
- @TransactionalEventListener(phase = AFTER_COMMIT)
174
- public void onOrderCancelled(OrderCancelledEvent event) {
175
- // proceso de reembolso, notificación al cliente
176
- }
177
- }
178
- ```
179
-
180
- ---
181
-
182
- ## 2. Aggregate Boundaries por ID ✅
183
-
184
- ### Descripción
185
-
186
- Eva4j ya genera correctamente el patrón DDD de referencia por ID: los campos que apuntan a otro agregado se generan como tipos primitivos (`String`, `Long`, etc.) sin ningún `@ManyToOne` cruzado. Esta feature añade **declaración semántica explícita** mediante la propiedad `reference:` en el campo, que permite documentar la intención en el YAML y generar un comentario Javadoc en el código.
187
-
188
- Sin `reference:`, un campo `customerId: String` es indistinguible de cualquier otro `String`. Con `reference:`, el generador sabe que es un puntero intencional al agregado `Customer` del módulo `customers`.
189
-
190
- ### Sintaxis
191
-
192
- ```yaml
193
- aggregates:
194
- - name: Order
195
- entities:
196
- - name: order
197
- isRoot: true
198
- fields:
199
- - name: id
200
- type: String
201
- - name: customerId
202
- type: String
203
- reference:
204
- aggregate: Customer # Nombre del agregado (PascalCase) — obligatorio
205
- module: customers # Módulo donde vive el agregado — opcional
206
- - name: productId
207
- type: String
208
- reference:
209
- aggregate: Product
210
- module: catalog
211
- ```
212
-
213
- ### Comportamiento
214
-
215
- - El tipo Java **no cambia** — sigue siendo `String`, `Long`, etc.
216
- - JPA genera `@Column` normal — **sin** `@ManyToOne` ni `@JoinColumn`.
217
- - Se genera un **comentario Javadoc** en la entidad de dominio y en la entidad JPA.
218
- - `module:` es opcional: se puede omitir si el agregado referenciado está en el mismo módulo.
219
- - Si `reference:` está malformado (falta `aggregate`), eva4j lanza un error descriptivo.
220
-
221
- ### Código Generado
222
-
223
- ```java
224
- // domain/models/entities/Order.java
225
- /** Cross-aggregate reference Customer (module: customers) */
226
- private String customerId;
227
-
228
- /** Cross-aggregate reference Product (module: catalog) */
229
- private String productId;
230
- ```
231
-
232
- ```java
233
- // infrastructure/database/entities/OrderJpa.java
234
- @Column(name = "customer_id")
235
- /** Cross-aggregate reference → Customer (module: customers) */
236
- private String customerId;
237
-
238
- @Column(name = "product_id")
239
- /** Cross-aggregate reference → Product (module: catalog) */
240
- private String productId;
241
- ```
242
-
243
- ### Archivos Modificados
244
-
245
- | Archivo | Cambio |
246
- |---|---|
247
- | `src/utils/yaml-to-entity.js` | ✅ Destructura y valida `reference:` en `parseProperty()` |
248
- | `templates/aggregate/AggregateRoot.java.ejs` | Genera comentario Javadoc en campos con `reference` |
249
- | `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
250
- | `templates/aggregate/JpaEntity.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
251
- | `examples/domain-multi-aggregate.yaml` | ✅ Actualizado con `reference:` en `productId` y `warehouseId` |
252
-
253
- ---
254
-
255
- ## 3. Soft Delete Completo
256
-
257
- ### Descripción
258
-
259
- El archivo de ejemplo `domain-soft-delete.yaml` existe pero la generación real del patrón no está completamente implementada. Soft delete es crítico en sistemas donde la normativa exige conservar registros históricos o donde el negocio necesita restaurar datos eliminados accidentalmente.
260
-
261
- ### Sintaxis en domain.yaml
262
-
263
- ```yaml
264
- entities:
265
- - name: product
266
- isRoot: true
267
- tableName: products
268
- softDelete: true
269
- fields:
270
- - name: id
271
- type: String
272
- - name: name
273
- type: String
274
- - name: price
275
- type: BigDecimal
276
- ```
277
-
278
- ### Código Generado
279
-
280
- ```java
281
- @MappedSuperclass
282
- public abstract class SoftDeletableEntity {
283
- @Column(name = "deleted", nullable = false)
284
- private Boolean deleted = false;
285
-
286
- @Column(name = "deleted_at")
287
- private LocalDateTime deletedAt;
288
-
289
- public void softDelete() {
290
- this.deleted = true;
291
- this.deletedAt = LocalDateTime.now();
292
- }
293
-
294
- public void restore() {
295
- this.deleted = false;
296
- this.deletedAt = null;
297
- }
298
- }
299
- ```
300
-
301
- ```java
302
- @Entity
303
- @Table(name = "products")
304
- @Where(clause = "deleted = false")
305
- @SQLDelete(sql = "UPDATE products SET deleted = true, deleted_at = NOW() WHERE id = ?")
306
- public class ProductJpa extends SoftDeletableEntity {
307
- @Id
308
- private String id;
309
- }
310
- ```
311
-
312
- ```java
313
- // DeleteCommandHandler actualizado
314
- public void handle(DeleteProductCommand command) {
315
- Product product = productRepository.findById(command.id()).orElseThrow();
316
- product.softDelete();
317
- productRepository.save(product);
318
- }
319
- ```
320
-
321
- ```java
322
- // Endpoint de restauración generado automáticamente
323
- @PatchMapping("/{id}/restore")
324
- public ResponseEntity<Void> restore(@PathVariable String id) {
325
- restoreProductUseCase.handle(new RestoreProductCommand(id));
326
- return ResponseEntity.noContent().build();
327
- }
328
- ```
329
-
330
- ---
331
-
332
- ## � MEDIA PRIORIDAD
333
-
334
- ---
335
-
336
- ## 4. Paginación en Queries ✅
337
-
338
- ### Descripción
339
-
340
- Implementado como **paginación siempre activa** en todos los módulos generados. `GET /` ya no devuelve `List<T>` sin límite — devuelve un `PagedResponse<T>` propio con `content`, `page`, `size`, `totalElements` y `totalPages`. Sin flags ni configuración adicional en `domain.yaml`.
341
-
342
- ### Implementación Realizada
343
-
344
- #### PagedResponse `shared/application/dtos/PagedResponse.java`
345
-
346
- Record genérico generado una vez por proyecto en la capa shared. Desacoplado de Spring Data `Page<T>` para no exponer internals de Spring en la API:
347
-
348
- ```java
349
- public record PagedResponse<T>(
350
- List<T> content,
351
- int page,
352
- int size,
353
- long totalElements,
354
- int totalPages
355
- ) {
356
- public static <T> PagedResponse<T> of(
357
- List<T> content, int page, int size, long totalElements) {
358
- int totalPages = size == 0 ? 1 : (int) Math.ceil((double) totalElements / size);
359
- return new PagedResponse<>(content, page, size, totalElements, totalPages);
360
- }
361
- }
362
- ```
363
-
364
- #### Query con parámetros de paginación
365
-
366
- ```java
367
- public record FindAllOrdersQuery(
368
- int page,
369
- int size,
370
- String sortBy,
371
- String sortDirection
372
- ) implements Query<PagedResponse<OrderResponseDto>> {}
373
- ```
374
-
375
- #### Handler paginado
376
-
377
- ```java
378
- public PagedResponse<OrderResponseDto> handle(FindAllOrdersQuery query) {
379
- Sort sort = Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy());
380
- Pageable pageable = PageRequest.of(query.page(), query.size(), sort);
381
- Page<Order> page = repository.findAll(pageable);
382
- List<OrderResponseDto> content = page.getContent().stream().map(mapper::toDto).toList();
383
- return PagedResponse.of(content, page.getNumber(), page.getSize(), page.getTotalElements());
384
- }
385
- ```
386
-
387
- #### Endpoint REST
388
-
389
- ```bash
390
- # Defaults: page=0, size=20, sortBy=id, sortDirection=ASC
391
- GET /api/v1/orders?page=0&size=10&sortBy=createdAt&sortDirection=DESC
392
-
393
- # Respuesta
394
- {
395
- "content": [...],
396
- "page": 0,
397
- "size": 10,
398
- "totalElements": 87,
399
- "totalPages": 9
400
- }
401
- ```
402
-
403
- #### Archivos modificados
404
-
405
- | Archivo | Cambio |
406
- |---|---|
407
- | `templates/shared/application/dtos/PagedResponse.java.ejs` | Nuevo template shared |
408
- | `src/generators/shared-generator.js` | ✅ Método `generatePagedResponse()` |
409
- | `src/commands/generate-entities.js` | ✅ Llama `generatePagedResponse` en cada `g entities` |
410
- | `templates/crud/ListQuery.java.ejs` | ✅ Parámetros de paginación |
411
- | `templates/crud/ListQueryHandler.java.ejs` | ✅ `PageRequest` + `PagedResponse` |
412
- | `templates/aggregate/AggregateRepository.java.ejs` | ✅ `Page<X> findAll(Pageable)` |
413
- | `templates/aggregate/AggregateRepositoryImpl.java.ejs` | ✅ Implementación `jpaRepository.findAll(pageable).map(...)` |
414
- | `templates/crud/Controller.java.ejs` | ✅ `@RequestParam` page/size/sortBy/sortDirection |
415
-
416
- ---
417
-
418
- ## 5. Optimistic Locking
419
-
420
- ### Descripción
421
-
422
- El **Optimistic Locking** previene la pérdida de actualizaciones cuando dos usuarios modifican el mismo registro simultáneamente. Sin él, la última escritura gana sin advertencia, causando pérdida de datos silenciosa.
423
-
424
- ### Sintaxis Propuesta
425
-
426
- ```yaml
427
- entities:
428
- - name: account
429
- isRoot: true
430
- audit:
431
- enabled: true
432
- optimisticLocking: true
433
- fields:
434
- - name: id
435
- type: String
436
- - name: balance
437
- type: BigDecimal
438
- ```
439
-
440
- ### Código Generado
441
-
442
- ```java
443
- @Entity
444
- public class AccountJpa extends AuditableEntity {
445
- @Id
446
- private String id;
447
-
448
- @Column(name = "balance")
449
- private BigDecimal balance;
450
-
451
- @Version
452
- @Column(name = "version", nullable = false)
453
- private Long version;
454
- }
455
- ```
456
-
457
- ```java
458
- // El UpdateCommand incluye la versión esperada
459
- public record UpdateAccountCommand(
460
- String id,
461
- BigDecimal newBalance,
462
- Long version // Si no coincide con la BD: HTTP 409 Conflict
463
- ) {}
464
- ```
465
-
466
- ```java
467
- // ControllerAdvice generado
468
- @ExceptionHandler(ObjectOptimisticLockingFailureException.class)
469
- public ResponseEntity<ErrorDto> handleOptimisticLock(ObjectOptimisticLockingFailureException ex) {
470
- return ResponseEntity.status(HttpStatus.CONFLICT)
471
- .body(new ErrorDto("CONFLICT", "The record was modified by another user. Please reload and retry."));
472
- }
473
- ```
474
-
475
- ---
476
-
477
- ## 6. Read Models Separados (Proyecciones)
478
-
479
- ### Descripción
480
-
481
- En CQRS puro, el lado de lectura puede tener su propio modelo optimizado para consultas, independiente del modelo de escritura. Los `*ResponseDto` actuales son transformaciones directas del dominio, suficiente para casos simples pero insuficientes para reportes o vistas que joinean múltiples agregados.
482
-
483
- ### Sintaxis Propuesta
484
-
485
- ```yaml
486
- aggregates:
487
- - name: Order
488
- readModels:
489
- - name: OrderSummary
490
- description: "Vista desnormalizada para listados"
491
- fields:
492
- - name: id
493
- type: String
494
- - name: orderNumber
495
- type: String
496
- - name: customerName
497
- type: String
498
- - name: totalAmount
499
- type: BigDecimal
500
- - name: itemCount
501
- type: Integer
502
- - name: status
503
- type: OrderStatus
504
- source: native_query
505
- ```
506
-
507
- ### Código Generado
508
-
509
- ```java
510
- public interface OrderSummaryProjection {
511
- String getId();
512
- String getOrderNumber();
513
- String getCustomerName();
514
- BigDecimal getTotalAmount();
515
- Integer getItemCount();
516
- OrderStatus getStatus();
517
- }
518
- ```
519
-
520
- ```java
521
- @Query(value = """
522
- SELECT
523
- o.id,
524
- o.order_number AS orderNumber,
525
- c.name AS customerName,
526
- o.total_amount AS totalAmount,
527
- COUNT(i.id) AS itemCount,
528
- o.status
529
- FROM orders o
530
- JOIN customers c ON c.id = o.customer_id
531
- LEFT JOIN order_items i ON i.order_id = o.id
532
- WHERE o.deleted = false
533
- GROUP BY o.id, c.name
534
- """, nativeQuery = true)
535
- Page<OrderSummaryProjection> findOrderSummaries(Pageable pageable);
536
- ```
537
-
538
- ---
539
-
540
- ## 7. Enums con Comportamiento y Transiciones ✅
541
-
542
- ### Descripción
543
-
544
- Los enums generados actualmente son solo listas de valores. En DDD, los enums frecuentemente encapsulan lógica de transición de estado — qué valores son válidos como siguiente estado, qué acciones se permiten. Esto elimina `if/switch` dispersos en el dominio.
545
-
546
- ### Sintaxis Propuesta
547
-
548
- ```yaml
549
- enums:
550
- - name: OrderStatus
551
- withTransitions: true
552
- values:
553
- - DRAFT
554
- - PLACED
555
- - CONFIRMED
556
- - SHIPPED
557
- - DELIVERED
558
- - CANCELLED
559
- transitions:
560
- DRAFT: [PLACED, CANCELLED]
561
- PLACED: [CONFIRMED, CANCELLED]
562
- CONFIRMED: [SHIPPED, CANCELLED]
563
- SHIPPED: [DELIVERED]
564
- DELIVERED: []
565
- CANCELLED: []
566
- ```
567
-
568
- ### Código Generado
569
-
570
- ```java
571
- public enum OrderStatus {
572
- DRAFT(Set.of("PLACED", "CANCELLED")),
573
- PLACED(Set.of("CONFIRMED", "CANCELLED")),
574
- CONFIRMED(Set.of("SHIPPED", "CANCELLED")),
575
- SHIPPED(Set.of("DELIVERED")),
576
- DELIVERED(Set.of()),
577
- CANCELLED(Set.of());
578
-
579
- private final Set<String> allowedTransitions;
580
-
581
- OrderStatus(Set<String> allowedTransitions) {
582
- this.allowedTransitions = allowedTransitions;
583
- }
584
-
585
- public boolean canTransitionTo(OrderStatus next) {
586
- return allowedTransitions.contains(next.name());
587
- }
588
-
589
- public void validateTransitionTo(OrderStatus next) {
590
- if (!canTransitionTo(next)) {
591
- throw new IllegalStateException(
592
- String.format("Cannot transition from %s to %s", this.name(), next.name())
593
- );
594
- }
595
- }
596
- }
597
- ```
598
-
599
- ```java
600
- // Uso en entidad de dominio — declarativo, sin if/switch
601
- public void confirm() {
602
- this.status.validateTransitionTo(OrderStatus.CONFIRMED);
603
- this.status = OrderStatus.CONFIRMED;
604
- }
605
-
606
- public void ship() {
607
- this.status.validateTransitionTo(OrderStatus.SHIPPED);
608
- this.status = OrderStatus.SHIPPED;
609
- }
610
- ```
611
-
612
- ---
613
-
614
- ## 8. Políticas y Especificaciones
615
-
616
- ### Descripción
617
-
618
- El **Specification Pattern** encapsula reglas de negocio complejas como objetos combinables. Es especialmente útil cuando las mismas reglas se aplican en múltiples lugares: validación al crear, filtrado en queries, reportes. Actualmente eva4j no genera ninguna infraestructura para este patrón.
619
-
620
- ### Sintaxis Propuesta
621
-
622
- ```yaml
623
- aggregates:
624
- - name: Order
625
- specifications:
626
- - name: OrderCanBeShipped
627
- description: "Una orden puede enviarse si está confirmada y tiene dirección de envío"
628
- - name: OrderIsOverdue
629
- description: "Una orden está vencida si lleva más de 30 días en estado PLACED"
630
- ```
631
-
632
- ### Código Generado
633
-
634
- ```java
635
- public interface Specification<T> {
636
- boolean isSatisfiedBy(T candidate);
637
-
638
- default Specification<T> and(Specification<T> other) {
639
- return candidate -> this.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate);
640
- }
641
-
642
- default Specification<T> or(Specification<T> other) {
643
- return candidate -> this.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate);
644
- }
645
-
646
- default Specification<T> not() {
647
- return candidate -> !this.isSatisfiedBy(candidate);
648
- }
649
- }
650
- ```
651
-
652
- ```java
653
- @Component
654
- public class OrderCanBeShippedSpecification implements Specification<Order> {
655
- @Override
656
- public boolean isSatisfiedBy(Order order) {
657
- return order.getStatus() == OrderStatus.CONFIRMED
658
- && order.getShippingAddress() != null;
659
- }
660
- }
661
- ```
662
-
663
- ```java
664
- @Component
665
- public class ShipOrderCommandHandler {
666
- private final OrderCanBeShippedSpecification canBeShipped;
667
-
668
- public void handle(ShipOrderCommand command) {
669
- Order order = orderRepository.findById(command.orderId()).orElseThrow();
670
- if (!canBeShipped.isSatisfiedBy(order)) {
671
- throw new OrderCannotBeShippedException(command.orderId());
672
- }
673
- order.ship();
674
- orderRepository.save(order);
675
- }
676
- }
677
- ```
678
-
679
- ---
680
-
681
- ## TOOLING Y CALIDAD
682
-
683
- ---
684
-
685
- ## 9. Validación de domain.yaml con JSON Schema
686
-
687
- ### Descripción
688
-
689
- Actualmente los errores en `domain.yaml` producen mensajes crípticos de Node.js en tiempo de ejecución. Un JSON Schema publicado permitiría validación inmediata en el editor (VS Code, IntelliJ) antes de ejecutar `eva4j g entities`, con autocompletado y documentación inline.
690
-
691
- ### Comportamiento Esperado
692
-
693
- Con el schema configurado, el editor mostraría errores como:
694
-
695
- ```
696
- domain.yaml:14:5 error Property "tipe" is not allowed. Did you mean "type"?
697
- domain.yaml:28:9 error "audit.trackUser" requires "audit.enabled: true"
698
- domain.yaml:41:7 error Relationship type "OneToFew" is not valid.
699
- Expected one of: OneToOne, OneToMany, ManyToOne, ManyToMany
700
- ```
701
-
702
- ### Implementación
703
-
704
- ```json
705
- {
706
- "": "http://json-schema.org/draft-07/schema#",
707
- "title": "eva4j domain.yaml",
708
- "type": "object",
709
- "required": ["aggregates"],
710
- "properties": {
711
- "aggregates": {
712
- "type": "array",
713
- "items": {
714
- "required": ["name", "entities"],
715
- "properties": {
716
- "name": { "type": "string", "pattern": "^[A-Z][a-zA-Z0-9]*$" },
717
- "entities": { "type": "array" }
718
- },
719
- "additionalProperties": false
720
- }
721
- }
722
- }
723
- }
724
- ```
725
-
726
- ```json
727
- // .vscode/settings.json (generado por eva4j create)
728
- {
729
- "yaml.schemas": {
730
- "https://eva4j.dev/schemas/domain-yaml.json": "domain.yaml"
731
- }
732
- }
733
- ```
734
-
735
- ---
736
-
737
- ## 10. Generación Incremental / Diff
738
-
739
- ### Descripción
740
-
741
- Implementado como **safe mode con checksums SHA-256**. `eva4j g entities` (y `g usecase`, `g resource`) detecta si un archivo generado fue modificado manualmente después de su generación y lo omite automáticamente en re-ejecuciones. El flag `--force` permite sobreescribir cuando se desea regenerar intencionalmente.
742
-
743
- ### Implementación Realizada
744
-
745
- #### ChecksumManager `src/utils/checksum-manager.js`
746
-
747
- Almacena hashes SHA-256 de cada archivo escrito en un archivo `.eva4j-checksums.json` por módulo (junto al `domain.yaml`). Métodos clave:
748
- - `wasModified(destPath, generatedContent)` — compara hash en disco vs hash almacenado
749
- - `recordWrite(destPath, content)` registra hash del archivo recién escrito
750
- - `save()` persiste la base de datos de checksums
751
-
752
- #### Safe mode en `renderAndWrite()` — `src/utils/template-engine.js`
753
-
754
- ```bash
755
- # Comportamiento por defecto (safe mode)
756
- eva4j g entities orders
757
-
758
- # Output:
759
- # Order.java -- regenerado (sin cambios previos)
760
- # OrderJpa.java -- regenerado (sin cambios previos)
761
- # ⚠️ SKIP OrderApplicationMapper.java -- omitido (modificado manualmente — use --force to overwrite)
762
- # ⚠️ SKIP CreateOrderCommandHandler.java -- omitido (modificado manualmente)
763
-
764
- # Con --force: sobreescribe todo
765
- eva4j g entities orders --force
766
- ```
767
-
768
- #### Comandos con safe mode integrado
769
-
770
- | Comando | Estado |
771
- |---|---|
772
- | `eva4j g entities <module>` | Integrado |
773
- | `eva4j g usecase <module> <name>` | ✅ Integrado |
774
- | `eva4j g resource <module>` | ✅ Integrado |
775
- | `eva4j create` / `eva4j add module` | ⚠️ Out of scope (archivos de scaffolding inicial, no se re-ejecutan) |
776
-
777
- #### Nota sobre portabilidad
778
-
779
- `.eva4j-checksums.json` está en `.gitignore` por diseño — es estado local de la máquina de desarrollo. En un `git clone` fresco, la primera re-ejecución regenerará todos los archivos (comportamiento correcto en ese contexto).
780
-
781
- ---
782
-
783
- ## 11. Comando `eva4j doctor`
784
-
785
- ### Descripción
786
-
787
- Un comando de análisis estático que examina el código del proyecto y detecta violaciones de los patrones DDD que eva4j promueve. Útil para onboarding de equipos y revisiones de arquitectura.
788
-
789
- ### Uso
790
-
791
- ```bash
792
- eva4j doctor
793
- eva4j doctor --module orders
794
- eva4j doctor --verbose
795
- ```
796
-
797
- ### Salida Esperada
798
-
799
- ```
800
- � eva4j doctor — Analizando proyecto...
801
-
802
- Módulo: orders
803
-
804
- Order.java:45
805
- Setter público detectado: setStatus(OrderStatus status)
806
- Recomendación: Reemplazar con método de negocio: confirm(), cancel(), etc.
807
-
808
- ❌ OrderItemJpa.java:12
809
- Falta @JoinColumn en relación inverse @ManyToOne
810
- Recomendación: Agregar @JoinColumn(name = "order_id", nullable = false)
811
-
812
- ⚠️ CreateOrderCommandHandler.java:67
813
- Lógica de negocio detectada fuera del dominio: totalAmount > 0
814
- Recomendación: Mover validación a Order.place() como invariante de dominio
815
-
816
- OrderRepository.java OK
817
- ✅ OrderMapper.java — OK
818
-
819
- � Resultado: 2 errores, 1 advertencia, 2 archivos OK
820
- ```
821
-
822
- ### Reglas Implementadas
823
-
824
- | Regla | Severidad | Descripción |
825
- |---|---|---|
826
- | No setters en dominio | ❌ Error | Detecta `set*` públicos en entidades de dominio |
827
- | No constructor vacío en dominio | ❌ Error | Detecta `public Entity()` sin parámetros |
828
- | Repositorio solo para raíz | ❌ Error | Detecta `Repository<SecondaryEntity>` |
829
- | FK cross-aggregate | ⚠️ Warn | `@ManyToOne` a entidad de otro agregado |
830
- | Lógica de negocio en handler | ⚠️ Warn | Condicionales complejos en CommandHandlers |
831
- | Value Object mutable | ⚠️ Warn | Value Objects con setters o campos non-final |
832
-
833
- ---
834
-
835
- ## 12. Tests Generados Completos
836
-
837
- ### Descripción
838
-
839
- Actualmente eva4j genera estructura de test básica. Para proyectos en producción, los tests deben cubrir invariantes de dominio, contrato de mappers y tests de integración de módulo con Spring Modulith.
840
-
841
- ### Tests de Dominio Generados
842
-
843
- ```java
844
- class OrderTest {
845
-
846
- @Test
847
- @DisplayName("Should create order with valid data")
848
- void shouldCreateOrder() {
849
- Order order = new Order("ORD-001", "CUST-123");
850
- assertThat(order.getOrderNumber()).isEqualTo("ORD-001");
851
- assertThat(order.getStatus()).isEqualTo(OrderStatus.DRAFT);
852
- }
853
-
854
- @Test
855
- @DisplayName("Should not allow adding item to cancelled order")
856
- void shouldRejectItemOnCancelledOrder() {
857
- Order order = new Order("ORD-001", "CUST-123");
858
- order.cancel("Test");
859
- assertThatThrownBy(() -> order.addItem("PROD-1", 2, BigDecimal.TEN))
860
- .isInstanceOf(IllegalStateException.class)
861
- .hasMessageContaining("cancelled");
862
- }
863
- }
864
- ```
865
-
866
- ### Tests de Mapper (roundtrip)
867
-
868
- ```java
869
- class OrderMapperTest {
870
- private final OrderMapper mapper = new OrderMapper();
871
-
872
- @Test
873
- @DisplayName("Domain -> JPA -> Domain roundtrip preserves all fields")
874
- void domainToJpaRoundtrip() {
875
- Order original = new Order("id-1", "ORD-001", "CUST-123",
876
- OrderStatus.DRAFT, LocalDateTime.now(), LocalDateTime.now());
877
- OrderJpa jpa = mapper.toJpa(original);
878
- Order restored = mapper.toDomain(jpa);
879
- assertThat(restored.getId()).isEqualTo(original.getId());
880
- assertThat(restored.getStatus()).isEqualTo(original.getStatus());
881
- }
882
- }
883
- ```
884
-
885
- ### Tests de Módulo con Spring Modulith
886
-
887
- ```java
888
- @ApplicationModuleTest
889
- class OrderModuleTest {
890
-
891
- @Test
892
- @DisplayName("Module is self-contained -- no illegal cross-module dependencies")
893
- void moduleShouldBeValid(ApplicationModules modules) {
894
- modules.verify();
895
- }
896
-
897
- @Test
898
- @DisplayName("Create order publishes OrderPlacedEvent")
899
- @Transactional
900
- void shouldPublishOrderPlacedEvent(
901
- @Autowired CreateOrderCommandHandler handler,
902
- AssertablePublishedEvents events
903
- ) {
904
- handler.handle(new CreateOrderCommand("ORD-001", "CUST-123"));
905
- events.assertThat()
906
- .contains(OrderPlacedEvent.class)
907
- .matching(e -> e.getAggregateId().equals("ORD-001"));
908
- }
909
- }
910
- ```
911
-
912
- ---
913
-
914
- ## IMPLEMENTADO
915
-
916
- ---
917
-
918
- ## 13. Auditoría (Implementada)
919
-
920
- | Característica | Sintaxis | Estado |
921
- |---|---|---|
922
- | Auditoría de tiempo | `audit: { enabled: true }` | ✅ Implementado |
923
- | Auditoría de usuario | `audit: { trackUser: true }` | ✅ Implementado |
924
- | `@EnableJpaAuditing` condicional | `auditorAwareRef` solo si `trackUser: true` | ✅ Implementado |
925
- | Regeneración de `Application.java` en `g entities` | Automático | ✅ Implementado |
926
-
927
- Cuando `trackUser: true` se generan automáticamente: `UserContextFilter`, `UserContextHolder`, `AuditorAwareImpl` y la anotación `@EnableJpaAuditing(auditorAwareRef = "auditorProvider")` en `Application.java`.
928
-
929
- Cuando solo `enabled: true` se genera `@EnableJpaAuditing` sin `auditorAwareRef`.
930
-
931
- ---
932
-
933
- ## 14. Validaciones JSR-303 (Implementado)
934
-
935
- Generación automática de anotaciones Bean Validation en `Create*Command` y `Create*Dto`. Las validaciones **nunca** se generan en entidades de dominio ni en campos `readOnly: true`.
936
-
937
- ### Sintaxis
938
-
939
- ```yaml
940
- fields:
941
- - name: email
942
- type: String
943
- validations:
944
- - type: Email
945
- message: "Email inválido"
946
- - type: NotBlank
947
- - name: age
948
- type: Integer
949
- validations:
950
- - type: Min
951
- value: 18
952
- - type: Max
953
- value: 120
954
- ```
955
-
956
- ### Código Generado
957
-
958
- ```java
959
- @Email(message = "Email inválido")
960
- @NotBlank
961
- private String email;
962
-
963
- @Min(value = 18)
964
- @Max(value = 120)
965
- private Integer age;
966
- ```
967
-
968
- ---
969
-
970
- ## 16. `defaultValue` para campos `readOnly` (Implementado)
971
-
972
- Permite especificar un valor inicial para campos `readOnly` directamente en `domain.yaml`. El valor se emite en el **constructor de creación** de la entidad de dominio y como field initializer con `@Builder.Default` en la entidad JPA.
973
-
974
- ### Sintaxis
975
-
976
- ```yaml
977
- entities:
978
- - name: order
979
- fields:
980
- - name: status
981
- type: OrderStatus
982
- readOnly: true
983
- defaultValue: PENDING # Enum value
984
-
985
- - name: totalAmount
986
- type: BigDecimal
987
- readOnly: true
988
- defaultValue: "0.00" # BigDecimal literal
989
-
990
- - name: itemCount
991
- type: Integer
992
- readOnly: true
993
- defaultValue: 0 # Integer literal
994
-
995
- - name: isActive
996
- type: Boolean
997
- readOnly: true
998
- defaultValue: true # Boolean literal
999
- ```
1000
-
1001
- ### Código Generado — Dominio
1002
-
1003
- ```java
1004
- // Constructor de creación — defaultValue asignado automáticamente
1005
- public Order(String orderNumber, String customerId) {
1006
- this.orderNumber = orderNumber;
1007
- this.customerId = customerId;
1008
- // readOnly fields initialized with defaultValue:
1009
- this.status = OrderStatus.PENDING;
1010
- this.totalAmount = new BigDecimal("0.00");
1011
- this.itemCount = 0;
1012
- this.isActive = true;
1013
- }
1014
- ```
1015
-
1016
- ### Código Generado JPA
1017
-
1018
- ```java
1019
- @Builder.Default
1020
- private OrderStatus status = OrderStatus.PENDING;
1021
-
1022
- @Builder.Default
1023
- private BigDecimal totalAmount = new BigDecimal("0.00");
1024
-
1025
- @Builder.Default
1026
- private Integer itemCount = 0;
1027
- ```
1028
-
1029
- ### Tipos soportados
1030
-
1031
- | Tipo Java | Ejemplo YAML | Java emitido |
1032
- |-----------|-------------|---------------|
1033
- | `String` | `defaultValue: hello` | `"hello"` |
1034
- | `Integer` / `Long` | `defaultValue: 0` | `0` / `0L` |
1035
- | `Boolean` | `defaultValue: false` | `false` |
1036
- | `BigDecimal` | `defaultValue: "0.00"` | `new BigDecimal("0.00")` |
1037
- | `LocalDateTime` | `defaultValue: now` | `LocalDateTime.now()` |
1038
- | `LocalDate` | `defaultValue: now` | `LocalDate.now()` |
1039
- | `Instant` | `defaultValue: now` | `Instant.now()` |
1040
- | `UUID` | `defaultValue: random` | `UUID.randomUUID()` |
1041
- | Enum | `defaultValue: ACTIVE` | `EnumType.ACTIVE` |
1042
-
1043
- ### Reglas
1044
-
1045
- - `defaultValue` **solo es válido** en campos con `readOnly: true`. Si se usa en un campo no-readOnly, se emite un warning y se ignora.
1046
- - El campo **sigue siendo readOnly** — no aparece en el constructor de negocio ni en `CreateDto`.
1047
- - En campos con `autoInit` (enum con `initialValue`), `defaultValue` es ignorado — `autoInit` tiene precedencia.
1048
-
1049
- ### Archivos Modificados
1050
-
1051
- | Archivo | Cambio |
1052
- |---|---|
1053
- | `src/utils/yaml-to-entity.js` | ✅ `computeJavaDefaultValue()` + `defaultValue` en `parseProperty()` |
1054
- | `templates/aggregate/AggregateRoot.java.ejs` | ✅ Emite `this.field = defaultValue` en constructor de creación |
1055
- | `templates/aggregate/DomainEntity.java.ejs` | ✅ Mismo cambio para entidades secundarias |
1056
- | `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ `@Builder.Default` + field initializer |
1057
- | `templates/aggregate/JpaEntity.java.ejs` | ✅ Mismo cambio para entidades JPA secundarias |
1058
- | `examples/domain-field-visibility.yaml` | ✅ Ejemplos con `defaultValue` en campos readOnly |
1059
-
1060
- ---
1061
-
1062
- ## 15. Transactional Outbox Pattern
1063
-
1064
- ### Descripción
1065
-
1066
- El **Transactional Outbox Pattern** es la evolución natural de los Domain Events implementados (ítem 1). Resuelve el caso donde el proceso muere después del commit de BD pero antes de que `ApplicationEventPublisher` llegue a publicar al broker externo — en ese escenario, el evento se pierde silenciosamente.
1067
-
1068
- El patrón garantiza **at-least-once delivery**: los eventos son almacenados en la misma transacción que el agregado y un proceso separado los publica de forma resiliente.
1069
-
1070
- Los Domain Events ya implementados (`ApplicationEventPublisher` + `@TransactionalEventListener(AFTER_COMMIT)`) son suficientes para la mayoría de sistemas. Esta feature es necesaria para dominios críticos: pagos, auditoría regulatoria, inventario en tiempo real.
1071
-
1072
- **Nota:** El puerto `MessageBroker` ya generado no requiere cambios — solo se añade la capa de persistencia intermedia.
1073
-
1074
- ### Flujo del Patrón
1075
-
1076
- ```
1077
- BD Transaction:
1078
- INSERT INTO orders ...
1079
- INSERT INTO outbox_events (type, payload, published=false) ← misma TX
1080
- COMMIT
1081
-
1082
- Proceso resiliente (polling o CDC con Debezium):
1083
- SELECT * FROM outbox_events WHERE published = false
1084
- Publica a Kafka / RabbitMQ / SNS
1085
- UPDATE outbox_events SET published = true
1086
- ```
1087
-
1088
- ### Sintaxis Propuesta en domain.yaml
1089
-
1090
- ```yaml
1091
- aggregates:
1092
- - name: Order
1093
- events:
1094
- - name: OrderPlaced
1095
- kafka: true
1096
- delivery: at-least-once # ← activa Outbox Pattern para este evento
1097
- fields:
1098
- - name: customerId
1099
- type: String
1100
- ```
1101
-
1102
- ### Código Generado (Outbox Table + Publisher)
1103
-
1104
- ```java
1105
- @Entity
1106
- @Table(name = "outbox_events")
1107
- public class OutboxEvent {
1108
- @Id
1109
- private String id;
1110
- private String aggregateType;
1111
- private String aggregateId;
1112
- private String eventType;
1113
- @Column(columnDefinition = "TEXT")
1114
- private String payload; // JSON serializado del evento
1115
- private boolean published = false;
1116
- private LocalDateTime createdAt;
1117
- private LocalDateTime publishedAt;
1118
- }
1119
- ```
1120
-
1121
- ```java
1122
- // OutboxEventPublisher proceso de polling (cada 5s via @Scheduled)
1123
- @Component
1124
- public class OutboxEventPublisher {
1125
- @Scheduled(fixedDelay = 5000)
1126
- @Transactional
1127
- public void publishPendingEvents() {
1128
- List<OutboxEvent> pending = outboxRepository.findByPublishedFalse();
1129
- pending.forEach(event -> {
1130
- messageBroker.publishRaw(event.getEventType(), event.getPayload());
1131
- event.markPublished();
1132
- });
1133
- }
1134
- }
1135
- ```
1136
-
1137
- ### Prerrequisito
1138
-
1139
- Domain Events (ítem 1) implementados y funcionando — este ítem solo añade persistencia intermedia, no reemplaza la arquitectura existente.
1140
-
1141
- ---
1142
-
1143
- ## Resumen de Prioridades
1144
-
1145
- | # | Característica | Prioridad | Complejidad | Estado |
1146
- |---|---|---|---|---|
1147
- | 1 | Domain Events | Alta | Alta | ✅ Implementado |
1148
- | 2 | Aggregate Boundaries por ID | Alta | Media | ✅ Implementado |
1149
- | 3 | Soft Delete Completo | Alta | Baja | Parcial |
1150
- | 4 | Paginación en Queries | Impl. | -- | ✅ Implementado |
1151
- | 5 | Optimistic Locking | Media | Baja | Pendiente |
1152
- | 6 | Read Models / Proyecciones | Media | Alta | Pendiente |
1153
- | 7 | Enums con Transiciones | Impl. | -- | Implementado |
1154
- | 8 | Specifications Pattern | Media | Media | Pendiente |
1155
- | 9 | JSON Schema para domain.yaml | Tooling | Media | Pendiente |
1156
- | 10 | Generacion Incremental | Tooling | -- | ✅ Implementado |
1157
- | 11 | eva4j doctor | Tooling | Media | Pendiente |
1158
- | 12 | Tests Completos | Tooling | Media | Pendiente |
1159
- | 13 | Auditoria completa | Impl. | -- | Implementado |
1160
- | 14 | Validaciones JSR-303 | Impl. | -- | ✅ Implementado |
1161
- | 15 | Transactional Outbox Pattern | Alta | Alta | Pendiente |
1162
- | 16 | `defaultValue` para campos `readOnly` | Impl. | -- | ✅ Implementado |
1163
-
1164
- ---
1165
-
1166
- **Ultima actualizacion:** 2026-03-04
1167
- **Version de eva4j:** 1.x
1168
- **Estado:** Documento de planificacion y referencia
1
+ # Características Futuras - eva4j
2
+
3
+ Este documento describe las mejoras planificadas para futuras versiones de eva4j, organizadas por prioridad. Cada sección incluye el contexto DDD correspondiente, la sintaxis YAML propuesta y ejemplos del código que se generaría.
4
+
5
+ ---
6
+
7
+ ## � Tabla de Contenidos
8
+
9
+ ### � Alta Prioridad
10
+ - [Domain Events](#1-domain-events)
11
+ - [Aggregate Boundaries por ID](#2-aggregate-boundaries-por-id)
12
+ - [Soft Delete Completo](#3-soft-delete-completo)
13
+ - [Transactional Outbox Pattern](#15-transactional-outbox-pattern)
14
+
15
+ ### Media Prioridad
16
+ - [Paginación en Queries](#4-paginación-en-queries)
17
+ - [Optimistic Locking](#5-optimistic-locking)
18
+ - [Read Models Separados](#6-read-models-separados-proyecciones)
19
+ - [Enums con Comportamiento y Transiciones](#7-enums-con-comportamiento-y-transiciones)
20
+ - [Políticas y Especificaciones](#8-políticas-y-especificaciones)
21
+
22
+ ### Tooling y Calidad
23
+ - [Validación de domain.yaml con JSON Schema](#9-validación-de-domainyaml-con-json-schema)
24
+ - [Generación Incremental / Diff](#10-generación-incremental--diff)
25
+ - [Comando eva4j doctor](#11-comando-eva4j-doctor)
26
+ - [Tests Generados Completos](#12-tests-generados-completos)
27
+ - [Diagrama Mermaid desde domain.yaml](#13-diagrama-mermaid-desde-domainyaml-eva-g-diagram)
28
+
29
+ ### 🚀 Prototyping
30
+ - [Mock Mode `eva build --mock`](#17-mock-mode--eva-build---mock)
31
+
32
+ ### Implementado
33
+ - [Domain Events](#1-domain-events)
34
+ - [Aggregate Boundaries por ID](#2-aggregate-boundaries-por-id)
35
+ - [Soft Delete Completo](#3-soft-delete-completo)
36
+ - [Paginación en Queries](#4-paginación-en-queries)
37
+ - [Enums con Comportamiento y Transiciones](#7-enums-con-comportamiento-y-transiciones)
38
+ - [Generación Incremental / Diff](#10-generación-incremental--diff)
39
+ - [Auditoría de Tiempo y Usuario](#15-auditoría-implementada)
40
+ - [Validaciones JSR-303](#14-validaciones-jsr-303-implementado)
41
+ - [`defaultValue` para campos `readOnly`](#16-defaultvalue-para-campos-readonly-implementado)
42
+ - [Mock Mode (`eva build --mock`)](#17-mock-mode--eva-build---mock)
43
+
44
+ ---
45
+
46
+ ## � ALTA PRIORIDAD
47
+
48
+ ---
49
+
50
+ ## 1. Domain Events ✅
51
+
52
+ ### Descripción
53
+
54
+ Los **Domain Events** son el patrón más fundamental de DDD que actualmente falta en eva4j. Un evento de dominio representa algo significativo que ocurrió en el negocio — un hecho pasado, no una intención futura. Son esenciales para:
55
+
56
+ - Comunicar cambios entre agregados sin acoplamiento directo
57
+ - Disparar side effects (emails, notificaciones, actualizaciones de proyecciones)
58
+ - Construir sistemas eventualmente consistentes
59
+
60
+ Sin eventos de dominio, la comunicación entre agregados obliga a dependencias directas que violan los límites de los bounded contexts.
61
+
62
+ ### Sintaxis Propuesta en domain.yaml
63
+
64
+ ```yaml
65
+ aggregates:
66
+ - name: Order
67
+ entities:
68
+ - name: order
69
+ isRoot: true
70
+ events:
71
+ - name: OrderPlaced
72
+ fields:
73
+ - name: orderId
74
+ type: String
75
+ - name: customerId
76
+ type: String
77
+ - name: totalAmount
78
+ type: BigDecimal
79
+ - name: OrderCancelled
80
+ fields:
81
+ - name: orderId
82
+ type: String
83
+ - name: reason
84
+ type: String
85
+ ```
86
+
87
+ ### Código Generado
88
+
89
+ #### Clase base DomainEvent
90
+
91
+ ```java
92
+ // shared/domain/DomainEvent.java
93
+ public abstract class DomainEvent {
94
+ private final String eventId;
95
+ private final LocalDateTime occurredOn;
96
+ private final String aggregateId;
97
+
98
+ protected DomainEvent(String aggregateId) {
99
+ this.eventId = UUID.randomUUID().toString();
100
+ this.occurredOn = LocalDateTime.now();
101
+ this.aggregateId = aggregateId;
102
+ }
103
+
104
+ public String getEventId() { return eventId; }
105
+ public LocalDateTime getOccurredOn() { return occurredOn; }
106
+ public String getAggregateId() { return aggregateId; }
107
+ }
108
+ ```
109
+
110
+ #### Evento específico generado
111
+
112
+ ```java
113
+ // domain/models/events/OrderPlacedEvent.java
114
+ public class OrderPlacedEvent extends DomainEvent {
115
+ private final String customerId;
116
+ private final BigDecimal totalAmount;
117
+
118
+ public OrderPlacedEvent(String orderId, String customerId, BigDecimal totalAmount) {
119
+ super(orderId);
120
+ this.customerId = customerId;
121
+ this.totalAmount = totalAmount;
122
+ }
123
+
124
+ public String getCustomerId() { return customerId; }
125
+ public BigDecimal getTotalAmount() { return totalAmount; }
126
+ }
127
+ ```
128
+
129
+ #### Raíz del agregado con eventos
130
+
131
+ ```java
132
+ // domain/models/entities/Order.java
133
+ public class Order {
134
+ private List<DomainEvent> domainEvents = new ArrayList<>();
135
+
136
+ public List<DomainEvent> getDomainEvents() {
137
+ return Collections.unmodifiableList(domainEvents);
138
+ }
139
+
140
+ public void clearDomainEvents() {
141
+ domainEvents.clear();
142
+ }
143
+
144
+ public void place(String customerId, BigDecimal total) {
145
+ this.status = OrderStatus.PLACED;
146
+ domainEvents.add(new OrderPlacedEvent(this.id, customerId, total));
147
+ }
148
+
149
+ public void cancel(String reason) {
150
+ if (this.status == OrderStatus.DELIVERED) {
151
+ throw new IllegalStateException("Cannot cancel a delivered order");
152
+ }
153
+ this.status = OrderStatus.CANCELLED;
154
+ domainEvents.add(new OrderCancelledEvent(this.id, reason));
155
+ }
156
+ }
157
+ ```
158
+
159
+ #### Publicación automática desde el repositorio
160
+
161
+ ```java
162
+ @Override
163
+ public Order save(Order order) {
164
+ OrderJpa jpa = mapper.toJpa(order);
165
+ repository.save(jpa);
166
+ order.getDomainEvents().forEach(eventPublisher::publishEvent);
167
+ order.clearDomainEvents();
168
+ return mapper.toDomain(jpa);
169
+ }
170
+ ```
171
+
172
+ #### Listener en otro módulo (sin acoplamiento)
173
+
174
+ ```java
175
+ @Component
176
+ public class OrderEventListener {
177
+ @EventListener
178
+ public void onOrderPlaced(OrderPlacedEvent event) {
179
+ // enviar email de confirmación, actualizar inventario, etc.
180
+ }
181
+
182
+ @TransactionalEventListener(phase = AFTER_COMMIT)
183
+ public void onOrderCancelled(OrderCancelledEvent event) {
184
+ // proceso de reembolso, notificación al cliente
185
+ }
186
+ }
187
+ ```
188
+
189
+ ---
190
+
191
+ ## 2. Aggregate Boundaries por ID ✅
192
+
193
+ ### Descripción
194
+
195
+ Eva4j ya genera correctamente el patrón DDD de referencia por ID: los campos que apuntan a otro agregado se generan como tipos primitivos (`String`, `Long`, etc.) sin ningún `@ManyToOne` cruzado. Esta feature añade **declaración semántica explícita** mediante la propiedad `reference:` en el campo, que permite documentar la intención en el YAML y generar un comentario Javadoc en el código.
196
+
197
+ Sin `reference:`, un campo `customerId: String` es indistinguible de cualquier otro `String`. Con `reference:`, el generador sabe que es un puntero intencional al agregado `Customer` del módulo `customers`.
198
+
199
+ ### Sintaxis
200
+
201
+ ```yaml
202
+ aggregates:
203
+ - name: Order
204
+ entities:
205
+ - name: order
206
+ isRoot: true
207
+ fields:
208
+ - name: id
209
+ type: String
210
+ - name: customerId
211
+ type: String
212
+ reference:
213
+ aggregate: Customer # Nombre del agregado (PascalCase) — obligatorio
214
+ module: customers # Módulo donde vive el agregado — opcional
215
+ - name: productId
216
+ type: String
217
+ reference:
218
+ aggregate: Product
219
+ module: catalog
220
+ ```
221
+
222
+ ### Comportamiento
223
+
224
+ - El tipo Java **no cambia** — sigue siendo `String`, `Long`, etc.
225
+ - JPA genera `@Column` normal **sin** `@ManyToOne` ni `@JoinColumn`.
226
+ - Se genera un **comentario Javadoc** en la entidad de dominio y en la entidad JPA.
227
+ - `module:` es opcional: se puede omitir si el agregado referenciado está en el mismo módulo.
228
+ - Si `reference:` está malformado (falta `aggregate`), eva4j lanza un error descriptivo.
229
+
230
+ ### Código Generado
231
+
232
+ ```java
233
+ // domain/models/entities/Order.java
234
+ /** Cross-aggregate reference → Customer (module: customers) */
235
+ private String customerId;
236
+
237
+ /** Cross-aggregate reference → Product (module: catalog) */
238
+ private String productId;
239
+ ```
240
+
241
+ ```java
242
+ // infrastructure/database/entities/OrderJpa.java
243
+ @Column(name = "customer_id")
244
+ /** Cross-aggregate reference → Customer (module: customers) */
245
+ private String customerId;
246
+
247
+ @Column(name = "product_id")
248
+ /** Cross-aggregate reference Product (module: catalog) */
249
+ private String productId;
250
+ ```
251
+
252
+ ### Archivos Modificados
253
+
254
+ | Archivo | Cambio |
255
+ |---|---|
256
+ | `src/utils/yaml-to-entity.js` | ✅ Destructura y valida `reference:` en `parseProperty()` |
257
+ | `templates/aggregate/AggregateRoot.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
258
+ | `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
259
+ | `templates/aggregate/JpaEntity.java.ejs` | Genera comentario Javadoc en campos con `reference` |
260
+ | `examples/domain-multi-aggregate.yaml` | ✅ Actualizado con `reference:` en `productId` y `warehouseId` |
261
+
262
+ ---
263
+
264
+ ## 3. Soft Delete Completo ✅
265
+
266
+ ### Descripción
267
+
268
+ Implementado como `hasSoftDelete: true` en la entidad raíz del agregado. El generador inyecta automáticamente el campo `deletedAt`, añade `@SQLRestriction("deleted_at IS NULL")` en la entidad JPA para filtrar eliminados en todas las queries, genera `softDelete()` e `isDeleted()` en el dominio, y cambia el `DeleteCommandHandler` a borrado lógico.
269
+
270
+ ### Sintaxis
271
+
272
+ ```yaml
273
+ entities:
274
+ - name: product
275
+ isRoot: true
276
+ tableName: products
277
+ hasSoftDelete: true
278
+ audit:
279
+ enabled: true
280
+ fields:
281
+ - name: id
282
+ type: String
283
+ - name: name
284
+ type: String
285
+ - name: price
286
+ type: BigDecimal
287
+ ```
288
+
289
+ ### Archivos Modificados
290
+
291
+ | Archivo | Cambio |
292
+ |---|---|
293
+ | `src/utils/yaml-to-entity.js` | ✅ Parsea `hasSoftDelete`, inyecta `deletedAt` en campo de entidad |
294
+ | `src/commands/generate-entities.js` | ✅ Propaga `hasSoftDelete` a todos los contextos, excluye `deletedAt` de comandos/respuestas |
295
+ | `templates/aggregate/AggregateRoot.java.ejs` | ✅ Excluye `deletedAt` del constructor de creación, genera `softDelete()` e `isDeleted()` |
296
+ | `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ Añade `@SQLRestriction("deleted_at IS NULL")` e import condicional |
297
+ | `templates/aggregate/AggregateRepository.java.ejs` | ✅ Elimina `deleteById()` del puerto cuando hay soft delete |
298
+ | `templates/aggregate/AggregateRepositoryImpl.java.ejs` | ✅ Mismo cambio en la implementación |
299
+ | `templates/crud/DeleteCommandHandler.java.ejs` | ✅ Dos ramas: `findById→softDelete()→save()` vs `deleteById()` |
300
+ | `examples/domain-soft-delete.yaml` | ✅ Reescrito con sintaxis `hasSoftDelete: true` |
301
+
302
+ ### Comportamiento generado
303
+
304
+ - `deletedAt` inyectado automáticamente — no declarar a mano en `fields:`
305
+ - `deletedAt` excluido de `CreateCommand`, `ResponseDto` (invisible en API)
306
+ - `GET /products` y `GET /products/{id}` nunca retornan registros eliminados
307
+ - `DELETE /products/{id}` sobre un registro ya eliminado retorna 404
308
+ - `deleteById()` eliminado del contrato del repositorio
309
+
310
+ ### Scope excluido
311
+
312
+ - Endpoint de restauración (`PATCH /{id}/restore`) — pendiente de implementar como use case adicional
313
+ - Query param `includeDeleted=true` — requiere query nativa separada
314
+
315
+ ---
316
+
317
+ ## � MEDIA PRIORIDAD
318
+
319
+ ---
320
+
321
+ ## 4. Paginación en Queries ✅
322
+
323
+ ### Descripción
324
+
325
+ Implementado como **paginación siempre activa** en todos los módulos generados. `GET /` ya no devuelve `List<T>` sin límite — devuelve un `PagedResponse<T>` propio con `content`, `page`, `size`, `totalElements` y `totalPages`. Sin flags ni configuración adicional en `domain.yaml`.
326
+
327
+ ### Implementación Realizada
328
+
329
+ #### PagedResponse — `shared/application/dtos/PagedResponse.java`
330
+
331
+ Record genérico generado una vez por proyecto en la capa shared. Desacoplado de Spring Data `Page<T>` para no exponer internals de Spring en la API:
332
+
333
+ ```java
334
+ public record PagedResponse<T>(
335
+ List<T> content,
336
+ int page,
337
+ int size,
338
+ long totalElements,
339
+ int totalPages
340
+ ) {
341
+ public static <T> PagedResponse<T> of(
342
+ List<T> content, int page, int size, long totalElements) {
343
+ int totalPages = size == 0 ? 1 : (int) Math.ceil((double) totalElements / size);
344
+ return new PagedResponse<>(content, page, size, totalElements, totalPages);
345
+ }
346
+ }
347
+ ```
348
+
349
+ #### Query con parámetros de paginación
350
+
351
+ ```java
352
+ public record FindAllOrdersQuery(
353
+ int page,
354
+ int size,
355
+ String sortBy,
356
+ String sortDirection
357
+ ) implements Query<PagedResponse<OrderResponseDto>> {}
358
+ ```
359
+
360
+ #### Handler paginado
361
+
362
+ ```java
363
+ public PagedResponse<OrderResponseDto> handle(FindAllOrdersQuery query) {
364
+ Sort sort = Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy());
365
+ Pageable pageable = PageRequest.of(query.page(), query.size(), sort);
366
+ Page<Order> page = repository.findAll(pageable);
367
+ List<OrderResponseDto> content = page.getContent().stream().map(mapper::toDto).toList();
368
+ return PagedResponse.of(content, page.getNumber(), page.getSize(), page.getTotalElements());
369
+ }
370
+ ```
371
+
372
+ #### Endpoint REST
373
+
374
+ ```bash
375
+ # Defaults: page=0, size=20, sortBy=id, sortDirection=ASC
376
+ GET /api/v1/orders?page=0&size=10&sortBy=createdAt&sortDirection=DESC
377
+
378
+ # Respuesta
379
+ {
380
+ "content": [...],
381
+ "page": 0,
382
+ "size": 10,
383
+ "totalElements": 87,
384
+ "totalPages": 9
385
+ }
386
+ ```
387
+
388
+ #### Archivos modificados
389
+
390
+ | Archivo | Cambio |
391
+ |---|---|
392
+ | `templates/shared/application/dtos/PagedResponse.java.ejs` | ✅ Nuevo template shared |
393
+ | `src/generators/shared-generator.js` | ✅ Método `generatePagedResponse()` |
394
+ | `src/commands/generate-entities.js` | ✅ Llama `generatePagedResponse` en cada `g entities` |
395
+ | `templates/crud/ListQuery.java.ejs` | ✅ Parámetros de paginación |
396
+ | `templates/crud/ListQueryHandler.java.ejs` | ✅ `PageRequest` + `PagedResponse` |
397
+ | `templates/aggregate/AggregateRepository.java.ejs` | ✅ `Page<X> findAll(Pageable)` |
398
+ | `templates/aggregate/AggregateRepositoryImpl.java.ejs` | ✅ Implementación `jpaRepository.findAll(pageable).map(...)` |
399
+ | `templates/crud/Controller.java.ejs` | ✅ `@RequestParam` page/size/sortBy/sortDirection |
400
+
401
+ ---
402
+
403
+ ## 5. Optimistic Locking
404
+
405
+ ### Descripción
406
+
407
+ El **Optimistic Locking** previene la pérdida de actualizaciones cuando dos usuarios modifican el mismo registro simultáneamente. Sin él, la última escritura gana sin advertencia, causando pérdida de datos silenciosa.
408
+
409
+ ### Sintaxis Propuesta
410
+
411
+ ```yaml
412
+ entities:
413
+ - name: account
414
+ isRoot: true
415
+ audit:
416
+ enabled: true
417
+ optimisticLocking: true
418
+ fields:
419
+ - name: id
420
+ type: String
421
+ - name: balance
422
+ type: BigDecimal
423
+ ```
424
+
425
+ ### Código Generado
426
+
427
+ ```java
428
+ @Entity
429
+ public class AccountJpa extends AuditableEntity {
430
+ @Id
431
+ private String id;
432
+
433
+ @Column(name = "balance")
434
+ private BigDecimal balance;
435
+
436
+ @Version
437
+ @Column(name = "version", nullable = false)
438
+ private Long version;
439
+ }
440
+ ```
441
+
442
+ ```java
443
+ // El UpdateCommand incluye la versión esperada
444
+ public record UpdateAccountCommand(
445
+ String id,
446
+ BigDecimal newBalance,
447
+ Long version // Si no coincide con la BD: HTTP 409 Conflict
448
+ ) {}
449
+ ```
450
+
451
+ ```java
452
+ // ControllerAdvice generado
453
+ @ExceptionHandler(ObjectOptimisticLockingFailureException.class)
454
+ public ResponseEntity<ErrorDto> handleOptimisticLock(ObjectOptimisticLockingFailureException ex) {
455
+ return ResponseEntity.status(HttpStatus.CONFLICT)
456
+ .body(new ErrorDto("CONFLICT", "The record was modified by another user. Please reload and retry."));
457
+ }
458
+ ```
459
+
460
+ ---
461
+
462
+ ## 6. Read Models Separados (Proyecciones)
463
+
464
+ ### Descripción
465
+
466
+ En CQRS puro, el lado de lectura puede tener su propio modelo optimizado para consultas, independiente del modelo de escritura. Los `*ResponseDto` actuales son transformaciones directas del dominio, suficiente para casos simples pero insuficientes para reportes o vistas que joinean múltiples agregados.
467
+
468
+ ### Sintaxis Propuesta
469
+
470
+ ```yaml
471
+ aggregates:
472
+ - name: Order
473
+ readModels:
474
+ - name: OrderSummary
475
+ description: "Vista desnormalizada para listados"
476
+ fields:
477
+ - name: id
478
+ type: String
479
+ - name: orderNumber
480
+ type: String
481
+ - name: customerName
482
+ type: String
483
+ - name: totalAmount
484
+ type: BigDecimal
485
+ - name: itemCount
486
+ type: Integer
487
+ - name: status
488
+ type: OrderStatus
489
+ source: native_query
490
+ ```
491
+
492
+ ### Código Generado
493
+
494
+ ```java
495
+ public interface OrderSummaryProjection {
496
+ String getId();
497
+ String getOrderNumber();
498
+ String getCustomerName();
499
+ BigDecimal getTotalAmount();
500
+ Integer getItemCount();
501
+ OrderStatus getStatus();
502
+ }
503
+ ```
504
+
505
+ ```java
506
+ @Query(value = """
507
+ SELECT
508
+ o.id,
509
+ o.order_number AS orderNumber,
510
+ c.name AS customerName,
511
+ o.total_amount AS totalAmount,
512
+ COUNT(i.id) AS itemCount,
513
+ o.status
514
+ FROM orders o
515
+ JOIN customers c ON c.id = o.customer_id
516
+ LEFT JOIN order_items i ON i.order_id = o.id
517
+ WHERE o.deleted = false
518
+ GROUP BY o.id, c.name
519
+ """, nativeQuery = true)
520
+ Page<OrderSummaryProjection> findOrderSummaries(Pageable pageable);
521
+ ```
522
+
523
+ ---
524
+
525
+ ## 7. Enums con Comportamiento y Transiciones ✅
526
+
527
+ ### Descripción
528
+
529
+ Los enums generados actualmente son solo listas de valores. En DDD, los enums frecuentemente encapsulan lógica de transición de estado — qué valores son válidos como siguiente estado, qué acciones se permiten. Esto elimina `if/switch` dispersos en el dominio.
530
+
531
+ ### Sintaxis Propuesta
532
+
533
+ ```yaml
534
+ enums:
535
+ - name: OrderStatus
536
+ withTransitions: true
537
+ values:
538
+ - DRAFT
539
+ - PLACED
540
+ - CONFIRMED
541
+ - SHIPPED
542
+ - DELIVERED
543
+ - CANCELLED
544
+ transitions:
545
+ DRAFT: [PLACED, CANCELLED]
546
+ PLACED: [CONFIRMED, CANCELLED]
547
+ CONFIRMED: [SHIPPED, CANCELLED]
548
+ SHIPPED: [DELIVERED]
549
+ DELIVERED: []
550
+ CANCELLED: []
551
+ ```
552
+
553
+ ### Código Generado
554
+
555
+ ```java
556
+ public enum OrderStatus {
557
+ DRAFT(Set.of("PLACED", "CANCELLED")),
558
+ PLACED(Set.of("CONFIRMED", "CANCELLED")),
559
+ CONFIRMED(Set.of("SHIPPED", "CANCELLED")),
560
+ SHIPPED(Set.of("DELIVERED")),
561
+ DELIVERED(Set.of()),
562
+ CANCELLED(Set.of());
563
+
564
+ private final Set<String> allowedTransitions;
565
+
566
+ OrderStatus(Set<String> allowedTransitions) {
567
+ this.allowedTransitions = allowedTransitions;
568
+ }
569
+
570
+ public boolean canTransitionTo(OrderStatus next) {
571
+ return allowedTransitions.contains(next.name());
572
+ }
573
+
574
+ public void validateTransitionTo(OrderStatus next) {
575
+ if (!canTransitionTo(next)) {
576
+ throw new IllegalStateException(
577
+ String.format("Cannot transition from %s to %s", this.name(), next.name())
578
+ );
579
+ }
580
+ }
581
+ }
582
+ ```
583
+
584
+ ```java
585
+ // Uso en entidad de dominio — declarativo, sin if/switch
586
+ public void confirm() {
587
+ this.status.validateTransitionTo(OrderStatus.CONFIRMED);
588
+ this.status = OrderStatus.CONFIRMED;
589
+ }
590
+
591
+ public void ship() {
592
+ this.status.validateTransitionTo(OrderStatus.SHIPPED);
593
+ this.status = OrderStatus.SHIPPED;
594
+ }
595
+ ```
596
+
597
+ ---
598
+
599
+ ## 8. Políticas y Especificaciones
600
+
601
+ ### Descripción
602
+
603
+ El **Specification Pattern** encapsula reglas de negocio complejas como objetos combinables. Es especialmente útil cuando las mismas reglas se aplican en múltiples lugares: validación al crear, filtrado en queries, reportes. Actualmente eva4j no genera ninguna infraestructura para este patrón.
604
+
605
+ ### Sintaxis Propuesta
606
+
607
+ ```yaml
608
+ aggregates:
609
+ - name: Order
610
+ specifications:
611
+ - name: OrderCanBeShipped
612
+ description: "Una orden puede enviarse si está confirmada y tiene dirección de envío"
613
+ - name: OrderIsOverdue
614
+ description: "Una orden está vencida si lleva más de 30 días en estado PLACED"
615
+ ```
616
+
617
+ ### Código Generado
618
+
619
+ ```java
620
+ public interface Specification<T> {
621
+ boolean isSatisfiedBy(T candidate);
622
+
623
+ default Specification<T> and(Specification<T> other) {
624
+ return candidate -> this.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate);
625
+ }
626
+
627
+ default Specification<T> or(Specification<T> other) {
628
+ return candidate -> this.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate);
629
+ }
630
+
631
+ default Specification<T> not() {
632
+ return candidate -> !this.isSatisfiedBy(candidate);
633
+ }
634
+ }
635
+ ```
636
+
637
+ ```java
638
+ @Component
639
+ public class OrderCanBeShippedSpecification implements Specification<Order> {
640
+ @Override
641
+ public boolean isSatisfiedBy(Order order) {
642
+ return order.getStatus() == OrderStatus.CONFIRMED
643
+ && order.getShippingAddress() != null;
644
+ }
645
+ }
646
+ ```
647
+
648
+ ```java
649
+ @Component
650
+ public class ShipOrderCommandHandler {
651
+ private final OrderCanBeShippedSpecification canBeShipped;
652
+
653
+ public void handle(ShipOrderCommand command) {
654
+ Order order = orderRepository.findById(command.orderId()).orElseThrow();
655
+ if (!canBeShipped.isSatisfiedBy(order)) {
656
+ throw new OrderCannotBeShippedException(command.orderId());
657
+ }
658
+ order.ship();
659
+ orderRepository.save(order);
660
+ }
661
+ }
662
+ ```
663
+
664
+ ---
665
+
666
+ ## TOOLING Y CALIDAD
667
+
668
+ ---
669
+
670
+ ## 9. Validación de domain.yaml con JSON Schema
671
+
672
+ ### Descripción
673
+
674
+ Actualmente los errores en `domain.yaml` producen mensajes crípticos de Node.js en tiempo de ejecución. Un JSON Schema publicado permitiría validación inmediata en el editor (VS Code, IntelliJ) antes de ejecutar `eva4j g entities`, con autocompletado y documentación inline.
675
+
676
+ ### Comportamiento Esperado
677
+
678
+ Con el schema configurado, el editor mostraría errores como:
679
+
680
+ ```
681
+ domain.yaml:14:5 error Property "tipe" is not allowed. Did you mean "type"?
682
+ domain.yaml:28:9 error "audit.trackUser" requires "audit.enabled: true"
683
+ domain.yaml:41:7 error Relationship type "OneToFew" is not valid.
684
+ Expected one of: OneToOne, OneToMany, ManyToOne, ManyToMany
685
+ ```
686
+
687
+ ### Implementación
688
+
689
+ ```json
690
+ {
691
+ "": "http://json-schema.org/draft-07/schema#",
692
+ "title": "eva4j domain.yaml",
693
+ "type": "object",
694
+ "required": ["aggregates"],
695
+ "properties": {
696
+ "aggregates": {
697
+ "type": "array",
698
+ "items": {
699
+ "required": ["name", "entities"],
700
+ "properties": {
701
+ "name": { "type": "string", "pattern": "^[A-Z][a-zA-Z0-9]*$" },
702
+ "entities": { "type": "array" }
703
+ },
704
+ "additionalProperties": false
705
+ }
706
+ }
707
+ }
708
+ }
709
+ ```
710
+
711
+ ```json
712
+ // .vscode/settings.json (generado por eva4j create)
713
+ {
714
+ "yaml.schemas": {
715
+ "https://eva4j.dev/schemas/domain-yaml.json": "domain.yaml"
716
+ }
717
+ }
718
+ ```
719
+
720
+ ---
721
+
722
+ ## 10. Generación Incremental / Diff ✅
723
+
724
+ ### Descripción
725
+
726
+ Implementado como **safe mode con checksums SHA-256**. `eva4j g entities` (y `g usecase`, `g resource`) detecta si un archivo generado fue modificado manualmente después de su generación y lo omite automáticamente en re-ejecuciones. El flag `--force` permite sobreescribir cuando se desea regenerar intencionalmente.
727
+
728
+ ### Implementación Realizada
729
+
730
+ #### ChecksumManager — `src/utils/checksum-manager.js`
731
+
732
+ Almacena hashes SHA-256 de cada archivo escrito en un archivo `.eva4j-checksums.json` por módulo (junto al `domain.yaml`). Métodos clave:
733
+ - `wasModified(destPath, generatedContent)` — compara hash en disco vs hash almacenado
734
+ - `recordWrite(destPath, content)` — registra hash del archivo recién escrito
735
+ - `save()` — persiste la base de datos de checksums
736
+
737
+ #### Safe mode en `renderAndWrite()` `src/utils/template-engine.js`
738
+
739
+ ```bash
740
+ # Comportamiento por defecto (safe mode)
741
+ eva4j g entities orders
742
+
743
+ # Output:
744
+ # ✅ Order.java -- regenerado (sin cambios previos)
745
+ # OrderJpa.java -- regenerado (sin cambios previos)
746
+ # ⚠️ SKIP OrderApplicationMapper.java -- omitido (modificado manualmente — use --force to overwrite)
747
+ # ⚠️ SKIP CreateOrderCommandHandler.java -- omitido (modificado manualmente)
748
+
749
+ # Con --force: sobreescribe todo
750
+ eva4j g entities orders --force
751
+ ```
752
+
753
+ #### Comandos con safe mode integrado
754
+
755
+ | Comando | Estado |
756
+ |---|---|
757
+ | `eva4j g entities <module>` | ✅ Integrado |
758
+ | `eva4j g usecase <module> <name>` | ✅ Integrado |
759
+ | `eva4j g resource <module>` | ✅ Integrado |
760
+ | `eva4j create` / `eva4j add module` | ⚠️ Out of scope (archivos de scaffolding inicial, no se re-ejecutan) |
761
+
762
+ #### Nota sobre portabilidad
763
+
764
+ `.eva4j-checksums.json` está en `.gitignore` por diseño — es estado local de la máquina de desarrollo. En un `git clone` fresco, la primera re-ejecución regenerará todos los archivos (comportamiento correcto en ese contexto).
765
+
766
+ ---
767
+
768
+ ## 11. Comando `eva4j doctor`
769
+
770
+ ### Descripción
771
+
772
+ Un comando de análisis estático que examina el código del proyecto y detecta violaciones de los patrones DDD que eva4j promueve. Útil para onboarding de equipos y revisiones de arquitectura.
773
+
774
+ ### Uso
775
+
776
+ ```bash
777
+ eva4j doctor
778
+ eva4j doctor --module orders
779
+ eva4j doctor --verbose
780
+ ```
781
+
782
+ ### Salida Esperada
783
+
784
+ ```
785
+ eva4j doctor — Analizando proyecto...
786
+
787
+ Módulo: orders
788
+
789
+ Order.java:45
790
+ Setter público detectado: setStatus(OrderStatus status)
791
+ Recomendación: Reemplazar con método de negocio: confirm(), cancel(), etc.
792
+
793
+ OrderItemJpa.java:12
794
+ Falta @JoinColumn en relación inverse @ManyToOne
795
+ Recomendación: Agregar @JoinColumn(name = "order_id", nullable = false)
796
+
797
+ ⚠️ CreateOrderCommandHandler.java:67
798
+ Lógica de negocio detectada fuera del dominio: totalAmount > 0
799
+ Recomendación: Mover validación a Order.place() como invariante de dominio
800
+
801
+ ✅ OrderRepository.java — OK
802
+ OrderMapper.java — OK
803
+
804
+ Resultado: 2 errores, 1 advertencia, 2 archivos OK
805
+ ```
806
+
807
+ ### Reglas Implementadas
808
+
809
+ | Regla | Severidad | Descripción |
810
+ |---|---|---|
811
+ | No setters en dominio | ❌ Error | Detecta `set*` públicos en entidades de dominio |
812
+ | No constructor vacío en dominio | ❌ Error | Detecta `public Entity()` sin parámetros |
813
+ | Repositorio solo para raíz | Error | Detecta `Repository<SecondaryEntity>` |
814
+ | FK cross-aggregate | ⚠️ Warn | `@ManyToOne` a entidad de otro agregado |
815
+ | Lógica de negocio en handler | ⚠️ Warn | Condicionales complejos en CommandHandlers |
816
+ | Value Object mutable | ⚠️ Warn | Value Objects con setters o campos non-final |
817
+
818
+ ---
819
+
820
+ ## 12. Tests Generados Completos
821
+
822
+ ### Descripción
823
+
824
+ Actualmente eva4j genera estructura de test básica. Para proyectos en producción, los tests deben cubrir invariantes de dominio, contrato de mappers y tests de integración de módulo con Spring Modulith.
825
+
826
+ ### Tests de Dominio Generados
827
+
828
+ ```java
829
+ class OrderTest {
830
+
831
+ @Test
832
+ @DisplayName("Should create order with valid data")
833
+ void shouldCreateOrder() {
834
+ Order order = new Order("ORD-001", "CUST-123");
835
+ assertThat(order.getOrderNumber()).isEqualTo("ORD-001");
836
+ assertThat(order.getStatus()).isEqualTo(OrderStatus.DRAFT);
837
+ }
838
+
839
+ @Test
840
+ @DisplayName("Should not allow adding item to cancelled order")
841
+ void shouldRejectItemOnCancelledOrder() {
842
+ Order order = new Order("ORD-001", "CUST-123");
843
+ order.cancel("Test");
844
+ assertThatThrownBy(() -> order.addItem("PROD-1", 2, BigDecimal.TEN))
845
+ .isInstanceOf(IllegalStateException.class)
846
+ .hasMessageContaining("cancelled");
847
+ }
848
+ }
849
+ ```
850
+
851
+ ### Tests de Mapper (roundtrip)
852
+
853
+ ```java
854
+ class OrderMapperTest {
855
+ private final OrderMapper mapper = new OrderMapper();
856
+
857
+ @Test
858
+ @DisplayName("Domain -> JPA -> Domain roundtrip preserves all fields")
859
+ void domainToJpaRoundtrip() {
860
+ Order original = new Order("id-1", "ORD-001", "CUST-123",
861
+ OrderStatus.DRAFT, LocalDateTime.now(), LocalDateTime.now());
862
+ OrderJpa jpa = mapper.toJpa(original);
863
+ Order restored = mapper.toDomain(jpa);
864
+ assertThat(restored.getId()).isEqualTo(original.getId());
865
+ assertThat(restored.getStatus()).isEqualTo(original.getStatus());
866
+ }
867
+ }
868
+ ```
869
+
870
+ ### Tests de Módulo con Spring Modulith
871
+
872
+ ```java
873
+ @ApplicationModuleTest
874
+ class OrderModuleTest {
875
+
876
+ @Test
877
+ @DisplayName("Module is self-contained -- no illegal cross-module dependencies")
878
+ void moduleShouldBeValid(ApplicationModules modules) {
879
+ modules.verify();
880
+ }
881
+
882
+ @Test
883
+ @DisplayName("Create order publishes OrderPlacedEvent")
884
+ @Transactional
885
+ void shouldPublishOrderPlacedEvent(
886
+ @Autowired CreateOrderCommandHandler handler,
887
+ AssertablePublishedEvents events
888
+ ) {
889
+ handler.handle(new CreateOrderCommand("ORD-001", "CUST-123"));
890
+ events.assertThat()
891
+ .contains(OrderPlacedEvent.class)
892
+ .matching(e -> e.getAggregateId().equals("ORD-001"));
893
+ }
894
+ }
895
+ ```
896
+
897
+ ---
898
+
899
+ ## 13. Diagrama Mermaid desde domain.yaml (`eva g diagram`)
900
+
901
+ ### Descripción
902
+
903
+ Generar automáticamente un diagrama **Mermaid `classDiagram`** a partir del `domain.yaml` de un módulo. El diagrama plasma visualmente la estructura del agregado: entidades, value objects, enums, relaciones y visibilidad de campos, usando estereotipos DDD.
904
+
905
+ El output es un archivo `.md` con el diagrama incrustado, renderizable nativamente en GitHub, VS Code (Markdown Preview), Notion y el reporte HTML de `eva evaluate system`.
906
+
907
+ ### Comando propuesto
908
+
909
+ ```bash
910
+ eva generate diagram <module>
911
+ eva g diagram <module> # alias corto
912
+ ```
913
+
914
+ ### Ejemplo de output para `domain-one-to-one.yaml`
915
+
916
+ ```mermaid
917
+ classDiagram
918
+ namespace UserAggregate {
919
+ class User {
920
+ <<aggregate root>>
921
+ +String id
922
+ +String username
923
+ +String email
924
+ -String passwordHash
925
+ +UserStatus status
926
+ +LocalDateTime registrationDate
927
+ +LocalDateTime lastLogin
928
+ +LocalDateTime createdAt
929
+ +LocalDateTime updatedAt
930
+ }
931
+
932
+ class UserProfile {
933
+ <<entity>>
934
+ +Long id
935
+ +String firstName
936
+ +String lastName
937
+ +LocalDate dateOfBirth
938
+ +String phoneNumber
939
+ +String bio
940
+ +String avatarUrl
941
+ +Boolean isPublic
942
+ }
943
+
944
+ class Address {
945
+ <<value object>>
946
+ +String street
947
+ +String city
948
+ +String state
949
+ +String zipCode
950
+ +String country
951
+ }
952
+
953
+ class UserPreferences {
954
+ <<value object>>
955
+ +String language
956
+ +String timezone
957
+ +Boolean emailNotifications
958
+ +Boolean smsNotifications
959
+ +String theme
960
+ }
961
+
962
+ class UserStatus {
963
+ <<enum>>
964
+ ACTIVE
965
+ INACTIVE
966
+ SUSPENDED
967
+ PENDING_VERIFICATION
968
+ DELETED
969
+ }
970
+ }
971
+
972
+ User "1" --o "1" UserProfile : owns
973
+ UserProfile *-- Address : embedded
974
+ UserProfile *-- UserPreferences : embedded
975
+ User --> UserStatus : status
976
+ ```
977
+
978
+ ### Reglas de generación
979
+
980
+ | Elemento domain.yaml | Stereotipo Mermaid | Visibilidad de campos |
981
+ |---|---|---|
982
+ | `isRoot: true` | `<<aggregate root>>` | `+` público, `-` hidden |
983
+ | Entidad secundaria | `<<entity>>` | igual |
984
+ | `valueObjects[]` | `<<value object>>` | `+` todos |
985
+ | `enums[]` | `<<enum>>` | valores como líneas |
986
+ | `audit.enabled: true` | campos `createdAt`, `updatedAt` en la entidad | auto-añadidos |
987
+ | `audit.trackUser: true` | campos `createdBy`, `updatedBy` en la entidad | auto-añadidos |
988
+ | `hidden: true` | prefijo `-` (privado) en lugar de `+` | |
989
+ | `readOnly: true` | prefijo `~` (package) para indicar derivado | |
990
+
991
+ ### Variante HTML (integración con `eva evaluate system`)
992
+
993
+ Otra opción es agregar un 5.º tab **"Dominio"** al reporte de `evaluate system` que renderice el diagrama Mermaid de cada módulo usando la librería [mermaid.js CDN](https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js), sin necesidad de un comando separado.
994
+
995
+ ### Estado
996
+
997
+ Implementado
998
+
999
+ ---
1000
+
1001
+ ## IMPLEMENTADO
1002
+
1003
+ ---
1004
+
1005
+ ## 15. Auditoría (Implementada)
1006
+
1007
+ | Característica | Sintaxis | Estado |
1008
+ |---|---|---|
1009
+ | Auditoría de tiempo | `audit: { enabled: true }` | ✅ Implementado |
1010
+ | Auditoría de usuario | `audit: { trackUser: true }` | ✅ Implementado |
1011
+ | `@EnableJpaAuditing` condicional | `auditorAwareRef` solo si `trackUser: true` | ✅ Implementado |
1012
+ | Regeneración de `Application.java` en `g entities` | Automático | ✅ Implementado |
1013
+
1014
+ Cuando `trackUser: true` se generan automáticamente: `UserContextFilter`, `UserContextHolder`, `AuditorAwareImpl` y la anotación `@EnableJpaAuditing(auditorAwareRef = "auditorProvider")` en `Application.java`.
1015
+
1016
+ Cuando solo `enabled: true` se genera `@EnableJpaAuditing` sin `auditorAwareRef`.
1017
+
1018
+ ---
1019
+
1020
+ ## 14. Validaciones JSR-303 (Implementado)
1021
+
1022
+ Generación automática de anotaciones Bean Validation en `Create*Command` y `Create*Dto`. Las validaciones **nunca** se generan en entidades de dominio ni en campos `readOnly: true`.
1023
+
1024
+ ### Sintaxis
1025
+
1026
+ ```yaml
1027
+ fields:
1028
+ - name: email
1029
+ type: String
1030
+ validations:
1031
+ - type: Email
1032
+ message: "Email inválido"
1033
+ - type: NotBlank
1034
+ - name: age
1035
+ type: Integer
1036
+ validations:
1037
+ - type: Min
1038
+ value: 18
1039
+ - type: Max
1040
+ value: 120
1041
+ ```
1042
+
1043
+ ### Código Generado
1044
+
1045
+ ```java
1046
+ @Email(message = "Email inválido")
1047
+ @NotBlank
1048
+ private String email;
1049
+
1050
+ @Min(value = 18)
1051
+ @Max(value = 120)
1052
+ private Integer age;
1053
+ ```
1054
+
1055
+ ---
1056
+
1057
+ ## 16. `defaultValue` para campos `readOnly` (Implementado)
1058
+
1059
+ Permite especificar un valor inicial para campos `readOnly` directamente en `domain.yaml`. El valor se emite en el **constructor de creación** de la entidad de dominio y como field initializer con `@Builder.Default` en la entidad JPA.
1060
+
1061
+ ### Sintaxis
1062
+
1063
+ ```yaml
1064
+ entities:
1065
+ - name: order
1066
+ fields:
1067
+ - name: status
1068
+ type: OrderStatus
1069
+ readOnly: true
1070
+ defaultValue: PENDING # Enum value
1071
+
1072
+ - name: totalAmount
1073
+ type: BigDecimal
1074
+ readOnly: true
1075
+ defaultValue: "0.00" # BigDecimal literal
1076
+
1077
+ - name: itemCount
1078
+ type: Integer
1079
+ readOnly: true
1080
+ defaultValue: 0 # Integer literal
1081
+
1082
+ - name: isActive
1083
+ type: Boolean
1084
+ readOnly: true
1085
+ defaultValue: true # Boolean literal
1086
+ ```
1087
+
1088
+ ### Código Generado Dominio
1089
+
1090
+ ```java
1091
+ // Constructor de creación — defaultValue asignado automáticamente
1092
+ public Order(String orderNumber, String customerId) {
1093
+ this.orderNumber = orderNumber;
1094
+ this.customerId = customerId;
1095
+ // readOnly fields initialized with defaultValue:
1096
+ this.status = OrderStatus.PENDING;
1097
+ this.totalAmount = new BigDecimal("0.00");
1098
+ this.itemCount = 0;
1099
+ this.isActive = true;
1100
+ }
1101
+ ```
1102
+
1103
+ ### Código Generado — JPA
1104
+
1105
+ ```java
1106
+ @Builder.Default
1107
+ private OrderStatus status = OrderStatus.PENDING;
1108
+
1109
+ @Builder.Default
1110
+ private BigDecimal totalAmount = new BigDecimal("0.00");
1111
+
1112
+ @Builder.Default
1113
+ private Integer itemCount = 0;
1114
+ ```
1115
+
1116
+ ### Tipos soportados
1117
+
1118
+ | Tipo Java | Ejemplo YAML | Java emitido |
1119
+ |-----------|-------------|---------------|
1120
+ | `String` | `defaultValue: hello` | `"hello"` |
1121
+ | `Integer` / `Long` | `defaultValue: 0` | `0` / `0L` |
1122
+ | `Boolean` | `defaultValue: false` | `false` |
1123
+ | `BigDecimal` | `defaultValue: "0.00"` | `new BigDecimal("0.00")` |
1124
+ | `LocalDateTime` | `defaultValue: now` | `LocalDateTime.now()` |
1125
+ | `LocalDate` | `defaultValue: now` | `LocalDate.now()` |
1126
+ | `Instant` | `defaultValue: now` | `Instant.now()` |
1127
+ | `UUID` | `defaultValue: random` | `UUID.randomUUID()` |
1128
+ | Enum | `defaultValue: ACTIVE` | `EnumType.ACTIVE` |
1129
+
1130
+ ### Reglas
1131
+
1132
+ - `defaultValue` **solo es válido** en campos con `readOnly: true`. Si se usa en un campo no-readOnly, se emite un warning y se ignora.
1133
+ - El campo **sigue siendo readOnly** — no aparece en el constructor de negocio ni en `CreateDto`.
1134
+ - En campos con `autoInit` (enum con `initialValue`), `defaultValue` es ignorado — `autoInit` tiene precedencia.
1135
+
1136
+ ### Archivos Modificados
1137
+
1138
+ | Archivo | Cambio |
1139
+ |---|---|
1140
+ | `src/utils/yaml-to-entity.js` | ✅ `computeJavaDefaultValue()` + `defaultValue` en `parseProperty()` |
1141
+ | `templates/aggregate/AggregateRoot.java.ejs` | ✅ Emite `this.field = defaultValue` en constructor de creación |
1142
+ | `templates/aggregate/DomainEntity.java.ejs` | ✅ Mismo cambio para entidades secundarias |
1143
+ | `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ `@Builder.Default` + field initializer |
1144
+ | `templates/aggregate/JpaEntity.java.ejs` | ✅ Mismo cambio para entidades JPA secundarias |
1145
+ | `examples/domain-field-visibility.yaml` | Ejemplos con `defaultValue` en campos readOnly |
1146
+
1147
+ ---
1148
+
1149
+ ## 15. Transactional Outbox Pattern
1150
+
1151
+ ### Descripción
1152
+
1153
+ El **Transactional Outbox Pattern** es la evolución natural de los Domain Events implementados (ítem 1). Resuelve el caso donde el proceso muere después del commit de BD pero antes de que `ApplicationEventPublisher` llegue a publicar al broker externo — en ese escenario, el evento se pierde silenciosamente.
1154
+
1155
+ El patrón garantiza **at-least-once delivery**: los eventos son almacenados en la misma transacción que el agregado y un proceso separado los publica de forma resiliente.
1156
+
1157
+ Los Domain Events ya implementados (`ApplicationEventPublisher` + `@TransactionalEventListener(AFTER_COMMIT)`) son suficientes para la mayoría de sistemas. Esta feature es necesaria para dominios críticos: pagos, auditoría regulatoria, inventario en tiempo real.
1158
+
1159
+ **Nota:** El puerto `MessageBroker` ya generado no requiere cambios solo se añade la capa de persistencia intermedia.
1160
+
1161
+ ### Flujo del Patrón
1162
+
1163
+ ```
1164
+ BD Transaction:
1165
+ → INSERT INTO orders ...
1166
+ INSERT INTO outbox_events (type, payload, published=false) ← misma TX
1167
+ COMMIT
1168
+
1169
+ Proceso resiliente (polling o CDC con Debezium):
1170
+ → SELECT * FROM outbox_events WHERE published = false
1171
+ → Publica a Kafka / RabbitMQ / SNS
1172
+ → UPDATE outbox_events SET published = true
1173
+ ```
1174
+
1175
+ ### Sintaxis Propuesta en domain.yaml
1176
+
1177
+ ```yaml
1178
+ aggregates:
1179
+ - name: Order
1180
+ events:
1181
+ - name: OrderPlaced
1182
+ kafka: true
1183
+ delivery: at-least-once # ← activa Outbox Pattern para este evento
1184
+ fields:
1185
+ - name: customerId
1186
+ type: String
1187
+ ```
1188
+
1189
+ ### Código Generado (Outbox Table + Publisher)
1190
+
1191
+ ```java
1192
+ @Entity
1193
+ @Table(name = "outbox_events")
1194
+ public class OutboxEvent {
1195
+ @Id
1196
+ private String id;
1197
+ private String aggregateType;
1198
+ private String aggregateId;
1199
+ private String eventType;
1200
+ @Column(columnDefinition = "TEXT")
1201
+ private String payload; // JSON serializado del evento
1202
+ private boolean published = false;
1203
+ private LocalDateTime createdAt;
1204
+ private LocalDateTime publishedAt;
1205
+ }
1206
+ ```
1207
+
1208
+ ```java
1209
+ // OutboxEventPublisher — proceso de polling (cada 5s via @Scheduled)
1210
+ @Component
1211
+ public class OutboxEventPublisher {
1212
+ @Scheduled(fixedDelay = 5000)
1213
+ @Transactional
1214
+ public void publishPendingEvents() {
1215
+ List<OutboxEvent> pending = outboxRepository.findByPublishedFalse();
1216
+ pending.forEach(event -> {
1217
+ messageBroker.publishRaw(event.getEventType(), event.getPayload());
1218
+ event.markPublished();
1219
+ });
1220
+ }
1221
+ }
1222
+ ```
1223
+
1224
+ ### Prerrequisito
1225
+
1226
+ Domain Events (ítem 1) implementados y funcionando — este ítem solo añade persistencia intermedia, no reemplaza la arquitectura existente.
1227
+
1228
+ ---
1229
+
1230
+ ## 🚀 PROTOTYPING
1231
+
1232
+ ---
1233
+
1234
+ ## 17. Mock Mode — `eva build --mock` ✅
1235
+
1236
+ ### El Problema
1237
+
1238
+ Hoy el pipeline de eva4j tiene un salto demasiado grande entre **especificación** y **ejecución**:
1239
+
1240
+ ```
1241
+ system.yaml + domain.yaml ──────────────────→ Código Java completo
1242
+ (requiere JVM, DB, Kafka, config)
1243
+ ```
1244
+
1245
+ Este salto crea un **cuello de botella** que afecta a todo el equipo:
1246
+
1247
+ 1. **Frontend bloqueado** — Los desarrolladores de UI no pueden construir pantallas hasta que el backend esté implementado, testeado y desplegado. Con un sistema de 5+ módulos, esto significa semanas de espera.
1248
+ 2. **Iteración lenta del diseño** — Cada cambio en el contrato API (agregar un campo, renombrar un endpoint, cambiar un flujo de estados) requiere regenerar código Java, compilar, levantar la base de datos y verificar. Un ciclo de retroalimentación de minutos cuando debería ser segundos.
1249
+ 3. **Infraestructura innecesaria en fase de diseño** — Para probar si los contratos entre módulos tienen sentido, no se necesita PostgreSQL, Kafka ni Docker. Se necesita un servidor que responda con datos realistas y respete las transiciones de estado.
1250
+ 4. **Desacoplamiento temporal frontend/backend** — El equipo de frontend y el equipo de backend deberían poder trabajar en paralelo desde el día uno, no secuencialmente.
1251
+
1252
+ ### La Visión
1253
+
1254
+ Introducir un **paso intermedio de prototipado** en el pipeline de eva4j que permita levantar un servidor funcional completo **sin infraestructura real**, directamente desde las especificaciones YAML:
1255
+
1256
+ ```
1257
+ ┌─ eva build --mock ────────────────────────┐
1258
+ system.yaml │ Spring Boot + H2 + Spring Events │
1259
+ + domain.yaml → │ Endpoints REST con Swagger UI │ ← Frontend trabaja aquí
1260
+ │ State machines reales │
1261
+ │ Eventos fluyendo entre módulos │
1262
+ │ Datos mock pre-cargados │
1263
+ └──────────────────────────────────────────┘
1264
+ ↓ (cuando el diseño estabiliza)
1265
+ ┌─ eva build ──────────────────────────────┐
1266
+ │ Spring Boot + PostgreSQL + Kafka │ ← Backend real
1267
+ └──────────────────────────────────────────┘
1268
+ ```
1269
+
1270
+ El mock mode genera **el mismo código de dominio** que la versión de producción — entidades, state machines, validaciones, use cases — pero reemplaza los adaptadores de infraestructura pesada (Kafka, PostgreSQL) por alternativas ligeras (Spring Events, H2). El 90% del código que corre en mock **es el mismo** que correrá en producción.
1271
+
1272
+ ### Por Qué Funciona: La Arquitectura Hexagonal Ya Lo Permite
1273
+
1274
+ La clave de esta feature es que **eva4j ya genera código con puertos y adaptadores**. El dominio nunca depende de la infraestructura. La cadena de eventos actual:
1275
+
1276
+ ```
1277
+ entity.raise(DomainEvent)
1278
+
1279
+ RepositoryImpl.save() → eventPublisher.publishEvent() ← Spring interno (ya existe)
1280
+
1281
+ DomainEventHandler @TransactionalEventListener(AFTER_COMMIT)
1282
+
1283
+ messageBroker.publish*(IntegrationEvent) ← Puerto abstracto (ya existe)
1284
+
1285
+ KafkaMessageBroker → kafkaTemplate.send() ← Adaptador Kafka
1286
+ ```
1287
+
1288
+ Para mock mode solo se necesita **otro adaptador** que reimplemente el mismo puerto:
1289
+
1290
+ ```
1291
+ messageBroker.publish*(IntegrationEvent)
1292
+
1293
+ InMemoryMessageBroker → applicationEventPublisher.publishEvent() ← Bus de Spring
1294
+ ```
1295
+
1296
+ Y en el lado consumidor, en vez de `@KafkaListener`:
1297
+
1298
+ ```
1299
+ @EventListener(condition = "#event.topic == 'BIKE_RESERVED'")
1300
+ public void handle(MockEvent event) {
1301
+ var payload = objectMapper.convertValue(event.data(), BikeReservedIntegrationEvent.class);
1302
+ useCaseMediator.dispatch(new InitiatePaymentCommand(...));
1303
+ }
1304
+ ```
1305
+
1306
+ **Es un swap de adaptadores.** El dominio, los use cases, los mappers, los DTOs, las validaciones — todo es idéntico.
1307
+
1308
+ ### Comando Propuesto
1309
+
1310
+ ```bash
1311
+ eva build --mock # Genera proyecto mock desde system/
1312
+ eva build --mock --port 3000 # Puerto custom
1313
+ eva build --mock --seed 20 # 20 entidades por módulo
1314
+ eva build --mock --dir ./my-system # Directorio de specs custom
1315
+ ```
1316
+
1317
+ ### Qué Genera el Comando
1318
+
1319
+ El comando orquesta la siguiente secuencia internamente:
1320
+
1321
+ ```
1322
+ 1. Lee system/system.yaml
1323
+ 2. eva create {name} --database h2 → Proyecto Spring Boot con H2
1324
+ 3. Para cada módulo:
1325
+ a. eva add module {name}
1326
+ b. Copia system/{module}.yaml → src/.../domain.yaml
1327
+ c. eva g entities {module} --broker inMemory → Genera InMemoryMessageBroker en vez de Kafka
1328
+ 4. Para cada listener en los domain.yaml:
1329
+ → Genera SpringEventListener en vez de KafkaListener
1330
+ 5. Genera MockDataSeeder.java → Datos fake pre-cargados
1331
+ 6. Genera application-mock.yaml → Profile Spring con H2 + config mock
1332
+ ```
1333
+
1334
+ ### Artefactos Nuevos (3 Templates + 1 Comando)
1335
+
1336
+ El esfuerzo es bajo porque la mayoría de artefactos **ya existen**. Solo se necesitan:
1337
+
1338
+ | Artefacto | Tipo | Propósito |
1339
+ |---|---|---|
1340
+ | `templates/mock/InMemoryMessageBroker.java.ejs` | Template | Adaptador mock que reemplaza `KafkaMessageBroker` |
1341
+ | `templates/mock/SpringEventListener.java.ejs` | Template | Listener que reemplaza `@KafkaListener` |
1342
+ | `templates/mock/MockDataSeeder.java.ejs` | Template | `CommandLineRunner` que siembra datos mock al arranque |
1343
+ | `templates/mock/application-mock.yaml.ejs` | Template | Profile Spring con H2 + configuración mock |
1344
+ | `src/commands/build-mock.js` | Comando | Orquestador del flujo completo |
1345
+
1346
+ ### Inventario: Qué Ya Existe vs Qué Falta
1347
+
1348
+ | Pieza | ¿Existe? | Mock Mode |
1349
+ |---|---|---|
1350
+ | Proyecto Spring Boot | `eva create` | ✅ Con `database: h2` |
1351
+ | Módulos | `eva add module` | ✅ Sin cambios |
1352
+ | Entidades, dominio, state machines | `eva g entities` | ✅ Sin cambios |
1353
+ | Endpoints REST + Swagger UI | `eva g entities` con `endpoints:` | ✅ Sin cambios |
1354
+ | Validaciones JSR-303 | Ya generadas | ✅ Sin cambios |
1355
+ | `ApplicationEventPublisher` en RepositoryImpl | Ya genera `eventPublisher.publishEvent()` | ✅ Sin cambios |
1356
+ | `DomainEventHandler` (`@TransactionalEventListener`) | Ya genera mapping Domain → Integration Event | ✅ Sin cambios |
1357
+ | Puerto `MessageBroker` (interfaz) | Ya genera `application/ports/MessageBroker.java` | ✅ Sin cambios |
1358
+ | **`InMemoryMessageBroker`** (adaptador mock) | **No existe** | 🆕 1 template |
1359
+ | **`SpringEventListener`** (reemplaza `@KafkaListener`) | **No existe** | 🆕 1 template |
1360
+ | **`MockDataSeeder`** (`CommandLineRunner`) | **No existe** | 🆕 1 template |
1361
+ | **Comando orquestador** | **No existe** | 🆕 1 comando |
1362
+
1363
+ De 12 piezas, **8 ya existen**. Solo se necesitan 4 artefactos nuevos.
1364
+
1365
+ ---
1366
+
1367
+ ### Detalle de Templates
1368
+
1369
+ #### 1. `InMemoryMessageBroker.java.ejs`
1370
+
1371
+ Implementa la misma interfaz `MessageBroker` que `KafkaMessageBroker`, pero re-publica al bus interno de Spring:
1372
+
1373
+ ```java
1374
+ @Component("{moduleCamelCase}InMemoryMessageBroker")
1375
+ public class {ModulePascal}InMemoryMessageBroker implements MessageBroker {
1376
+
1377
+ private final ApplicationEventPublisher eventPublisher;
1378
+ private final ObjectMapper objectMapper;
1379
+
1380
+ // constructor...
1381
+
1382
+ @Override
1383
+ public void publish{EventName}({EventName}IntegrationEvent event) {
1384
+ // Envuelve en MockEvent para routing cross-módulo
1385
+ Map<String, Object> payload = objectMapper.convertValue(event, Map.class);
1386
+ eventPublisher.publishEvent(new MockEvent("{TOPIC_NAME}", payload));
1387
+ }
1388
+ }
1389
+ ```
1390
+
1391
+ `MockEvent` es un record genérico en `shared`:
1392
+
1393
+ ```java
1394
+ public record MockEvent(String topic, Map<String, Object> data) {}
1395
+ ```
1396
+
1397
+ #### 2. `SpringEventListener.java.ejs`
1398
+
1399
+ Reemplaza `@KafkaListener`. Escucha `MockEvent` del bus de Spring y filtra por topic:
1400
+
1401
+ ```java
1402
+ @Component
1403
+ @Profile("mock")
1404
+ public class {EventName}SpringListener {
1405
+
1406
+ private final UseCaseMediator useCaseMediator;
1407
+ private final ObjectMapper objectMapper;
1408
+
1409
+ // constructor...
1410
+
1411
+ @EventListener(condition = "#event.topic() == '{TOPIC_NAME}'")
1412
+ public void handle(MockEvent event) {
1413
+ var payload = objectMapper.convertValue(
1414
+ event.data(), {EventName}IntegrationEvent.class
1415
+ );
1416
+ useCaseMediator.dispatch(new {UseCase}Command(
1417
+ // map event fields → command fields
1418
+ ));
1419
+ }
1420
+ }
1421
+ ```
1422
+
1423
+ Cada módulo mantiene su propio `IntegrationEvent` record (ya generado por `listeners[]`). La deserialización con `objectMapper.convertValue()` replica el mismo patrón que el `KafkaListener` real — los módulos siguen aislados.
1424
+
1425
+ #### 3. `MockDataSeeder.java.ejs`
1426
+
1427
+ Un `CommandLineRunner` activo solo con profile `mock` que siembra datos realistas usando los **Commands reales** del sistema:
1428
+
1429
+ ```java
1430
+ @Component
1431
+ public class MockDataSeeder implements CommandLineRunner {
1432
+
1433
+ private final UseCaseMediator mediator;
1434
+
1435
+ @Override
1436
+ public void run(String... args) {
1437
+ log.info("🌱 Mock data seeder started");
1438
+
1439
+ // Orden topológico derivado de references + integrations
1440
+ seedStations(5);
1441
+ seedBikes(15);
1442
+ seedAccounts(10);
1443
+ seedReservations(8);
1444
+
1445
+ log.info("✅ Mock data seeding complete");
1446
+ }
1447
+
1448
+ private void seedAccounts(int count) {
1449
+ for (int i = 0; i < count; i++) {
1450
+ mediator.dispatch(new CreateAccountCommand(
1451
+ "user" + i + "@example.com", // email
1452
+ "User " + i, // fullName
1453
+ "+1-555-" + String.format("%04d", i) // phone
1454
+ ));
1455
+ }
1456
+ }
1457
+ // ... métodos por módulo
1458
+ }
1459
+ ```
1460
+
1461
+ **Principio clave:** Al usar `mediator.dispatch()` con los Commands reales, **toda la cadena de eventos se activa automáticamente**. Crear una reserva dispara `BikeReservedEvent` → `payments.InitiatePayment` → `PaymentApprovedEvent` → `reservations.ConfirmReservation`. El seeder no necesita orquestar nada — los eventos fluyen solos vía el `InMemoryMessageBroker`.
1462
+
1463
+ ### Generación de Datos Mock: Derivación desde domain.yaml
1464
+
1465
+ La metadata ya presente en los domain.yaml es suficiente para generar valores fake inteligentes:
1466
+
1467
+ | Señal en el YAML | Estrategia de generación |
1468
+ |---|---|
1469
+ | `type: String` + nombre contiene `email` | `"user{i}@example.com"` |
1470
+ | `type: String` + nombre contiene `name`/`fullName` | `"Name {i}"` |
1471
+ | `type: String` + nombre contiene `phone` | `"+1-555-{i:04d}"` |
1472
+ | `type: String` + nombre contiene `url` | `"https://example.com/{name}/{i}"` |
1473
+ | `type: String` + nombre contiene `code` + `@Column(unique)` | `"CODE-{i}"` (garantiza unicidad) |
1474
+ | `type: String` + `reference: { aggregate: X }` | **ID de una entidad X ya creada** (round-robin) |
1475
+ | `type: BigDecimal` + validación `Positive` | `new BigDecimal("{10 + i * 5}")` |
1476
+ | `type: Integer` + validación `Min(value: N)` | `N + i` |
1477
+ | `type: LocalDateTime` + nombre contiene `At`/`Time` | `LocalDateTime.now().plusHours({i})` |
1478
+ | `type: {EnumName}` (coincide con `enums[]`) | Primer valor del enum (o random pick) |
1479
+ | `type: {ValueObject}` | Generación recursiva por fields del VO |
1480
+ | `readOnly: true` | **Omitir** — el dominio lo asigna (initialValue, defaultValue) |
1481
+ | `hidden: true` | Generar valor (ej: token) — no es visible en response pero sí en create |
1482
+
1483
+ Esta lógica reutiliza y extiende la función `generateDummyValue()` que ya existe en `templates/postman/Collection.json.ejs` para la colección Postman.
1484
+
1485
+ ### Orden de Seeding: Resolución Topológica
1486
+
1487
+ El `system.yaml` ya define el grafo de dependencias implícitamente. El seeder calcula el orden correcto:
1488
+
1489
+ 1. **`reference:` en fields** — `reservations.userId → accounts` implica que `accounts` se siembra antes.
1490
+ 2. **`integrations.async[]`** — `BikeCreatedEvent: fleet → reservations` implica que `fleet` se siembra antes.
1491
+ 3. **`integrations.sync[]`** — `reservations calls accounts` confirma la dependencia.
1492
+
1493
+ Resultado para el sistema de bicicletas:
1494
+
1495
+ ```
1496
+ 1. fleet (stations, bikes) ← sin dependencias
1497
+ 2. accounts (accounts) ← sin dependencias
1498
+ 3. reservations (reservations) ← depende de fleet + accounts
1499
+ 4. payments (se crea vía eventos) ← no necesita seed directo
1500
+ ```
1501
+
1502
+ ### Datos Declarativos Opcionales: `mock-data.yaml`
1503
+
1504
+ Para equipos que necesitan datos específicos (un usuario "demo", un producto con ID conocido):
1505
+
1506
+ ```yaml
1507
+ # mock-data.yaml — opcional, overrides los datos auto-generados
1508
+ seed:
1509
+ accounts:
1510
+ count: 10 # 10 cuentas totales
1511
+ data: # las primeras se crean con datos explícitos
1512
+ - email: "demo@example.com"
1513
+ fullName: "Demo User"
1514
+ phone: "+1234567890"
1515
+ # las restantes 9 se generan automáticamente
1516
+
1517
+ fleet:
1518
+ stations:
1519
+ count: 5
1520
+ bikes:
1521
+ count: 15
1522
+
1523
+ reservations:
1524
+ count: 8
1525
+ ```
1526
+
1527
+ ### Comportamiento del Sistema en Mock Mode
1528
+
1529
+ El frontend dev ejecuta `eva build --mock`, espera la compilación, y obtiene:
1530
+
1531
+ ```
1532
+ $ cd generated-project && ./gradlew bootRun --args='--spring.profiles.active=mock'
1533
+
1534
+ 🚀 test-eva started on port 8080 (profile: mock)
1535
+
1536
+ Modules: reservations, fleet, accounts, payments, notifications
1537
+ Database: H2 (in-memory, ddl-auto: create-drop)
1538
+ Events: Spring ApplicationEventPublisher (in-process)
1539
+ Swagger: http://localhost:8080/swagger-ui.html
1540
+
1541
+ 🌱 Mock data seeded:
1542
+ 5 stations, 15 bikes, 10 accounts, 8 reservations
1543
+ Events propagated: 8 BikeReserved → 8 PaymentApproved → 8 ReservationConfirmed
1544
+ ```
1545
+
1546
+ #### El flujo completo que ve un frontend dev
1547
+
1548
+ ```
1549
+ Frontend: POST /reservations { userId, bikeId, stationId, amount, scheduledPickupTime }
1550
+
1551
+ Mock: crea reserva (status: PENDING_PAYMENT) → 201 Created
1552
+ ↓ raise(BikeReservedEvent)
1553
+ ↓ InMemoryMessageBroker → publishEvent(MockEvent("BIKE_RESERVED", payload))
1554
+ ↓ SpringEventListener en payments → crea Payment (PENDING)
1555
+ ↓ handler auto-aprueba → raise(PaymentApprovedEvent)
1556
+ ↓ InMemoryMessageBroker → publishEvent(MockEvent("PAYMENT_APPROVED", payload))
1557
+ ↓ SpringEventListener en reservations → confirm() → status: CONFIRMED
1558
+
1559
+ Frontend: GET /reservations/{id}
1560
+ → { status: "CONFIRMED", ... } ← cambió automáticamente
1561
+
1562
+ Frontend: PUT /reservations/{id}/pickup
1563
+ → { status: "IN_PROGRESS", ... }
1564
+ ↓ state machine valida: CONFIRMED → IN_PROGRESS ✅
1565
+
1566
+ Frontend: PUT /reservations/{id}/return
1567
+ → { status: "COMPLETED", ... }
1568
+ ↓ raise(TripCompletedEvent) → accounts.HandleTripCompleted
1569
+ ```
1570
+
1571
+ El frontend dev ve **exactamente el mismo comportamiento** que tendría el sistema real, incluyendo transiciones de estado asíncronas y reacciones cross-módulo.
1572
+
1573
+ ### Transición de Mock a Producción
1574
+
1575
+ Cambiar de mock a producción **no requiere regenerar código de dominio**. Solo:
1576
+
1577
+ 1. `system.yaml` → `database: postgresql` (en vez de `h2`)
1578
+ 2. `eva add kafka-client` → instala adaptadores Kafka reales
1579
+ 3. `eva g entities {module}` → regenera adaptadores (ya usa `--broker kafka` por defecto)
1580
+ 4. Configurar `application-production.yaml` con URLs reales
1581
+
1582
+ El dominio, los use cases, los mappers, los DTOs, las validaciones — **todo permanece idéntico**.
1583
+
1584
+ ### Dependencias Adicionales
1585
+
1586
+ Solo una librería nueva (opcional, para datos más realistas):
1587
+
1588
+ ```gradle
1589
+ // build.gradle — solo en profile mock
1590
+ runtimeOnly 'net.datafaker:datafaker:2.4.2' // Sucesor de JavaFaker, mantenido activamente
1591
+ ```
1592
+
1593
+ Sin DataFaker, el seeder genera datos con patrones simples (`"User 0"`, `"user0@example.com"`). Con DataFaker, genera nombres, emails, teléfonos y direcciones realistas en cualquier locale.
1594
+
1595
+ ### Estado
1596
+
1597
+ ⏳ Pendiente de implementación
1598
+
1599
+ ---
1600
+
1601
+ ## Resumen de Prioridades
1602
+
1603
+ | # | Característica | Prioridad | Complejidad | Estado |
1604
+ |---|---|---|---|---|
1605
+ | 1 | Domain Events | Alta | Alta | ✅ Implementado |
1606
+ | 2 | Aggregate Boundaries por ID | Alta | Media | ✅ Implementado |
1607
+ | 3 | Soft Delete Completo | Alta | Baja | ✅ Implementado |
1608
+ | 4 | Paginación en Queries | Impl. | -- | ✅ Implementado |
1609
+ | 5 | Optimistic Locking | Media | Baja | Pendiente |
1610
+ | 6 | Read Models / Proyecciones | Media | Alta | Pendiente |
1611
+ | 7 | Enums con Transiciones | Impl. | -- | ✅ Implementado |
1612
+ | 8 | Specifications Pattern | Media | Media | Pendiente |
1613
+ | 9 | JSON Schema para domain.yaml | Tooling | Media | Pendiente |
1614
+ | 10 | Generacion Incremental | Tooling | -- | ✅ Implementado |
1615
+ | 11 | eva4j doctor | Tooling | Media | Pendiente |
1616
+ | 12 | Tests Completos | Tooling | Media | Pendiente |
1617
+ | 13 | Auditoria completa | Impl. | -- | ✅ Implementado |
1618
+ | 14 | Validaciones JSR-303 | Impl. | -- | ✅ Implementado |
1619
+ | 15 | Transactional Outbox Pattern | Alta | Alta | Pendiente |
1620
+ | 16 | `defaultValue` para campos `readOnly` | Impl. | -- | ✅ Implementado |
1621
+ | 17 | Mock Mode (`eva build --mock`) | Alta | Media | ✅ Implementado |
1622
+
1623
+ ---
1624
+
1625
+ **Ultima actualizacion:** 2026-03-13
1626
+ **Version de eva4j:** 1.x
1627
+ **Estado:** Documento de planificacion y referencia