eva4j 1.0.14 → 1.0.16

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 (139) hide show
  1. package/.agents/skills/skill-creator/LICENSE.txt +202 -0
  2. package/.agents/skills/skill-creator/SKILL.md +485 -0
  3. package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
  4. package/.agents/skills/skill-creator/agents/comparator.md +202 -0
  5. package/.agents/skills/skill-creator/agents/grader.md +223 -0
  6. package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
  7. package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  8. package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  9. package/.agents/skills/skill-creator/references/schemas.md +430 -0
  10. package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  11. package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  12. package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
  13. package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
  14. package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
  15. package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
  16. package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
  17. package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
  18. package/.agents/skills/skill-creator/scripts/utils.py +47 -0
  19. package/AGENTS.md +268 -6
  20. package/COMMAND_EVALUATION.md +15 -16
  21. package/DOMAIN_YAML_GUIDE.md +430 -14
  22. package/FUTURE_FEATURES.md +1627 -1168
  23. package/README.md +461 -13
  24. package/bin/eva4j.js +32 -14
  25. package/config/defaults.json +1 -0
  26. package/docs/commands/EVALUATE_SYSTEM.md +746 -261
  27. package/docs/commands/EXPORT_DIAGRAM.md +153 -0
  28. package/docs/commands/GENERATE_ENTITIES.md +599 -6
  29. package/docs/commands/INDEX.md +7 -0
  30. package/examples/domain-events.yaml +166 -20
  31. package/examples/domain-listeners.yaml +212 -0
  32. package/examples/domain-one-to-many.yaml +1 -0
  33. package/examples/domain-one-to-one.yaml +1 -0
  34. package/examples/domain-ports.yaml +414 -0
  35. package/examples/domain-soft-delete.yaml +47 -44
  36. package/examples/system/notification.yaml +147 -0
  37. package/examples/system/product.yaml +185 -0
  38. package/examples/system/system.yaml +112 -0
  39. package/examples/system-report.html +971 -0
  40. package/examples/system.yaml +46 -3
  41. package/package.json +2 -1
  42. package/src/agents/design-reviewer.agent.md +163 -0
  43. package/src/commands/build.js +714 -0
  44. package/src/commands/create.js +1 -0
  45. package/src/commands/detach.js +149 -108
  46. package/src/commands/evaluate-system.js +234 -8
  47. package/src/commands/export-diagram.js +522 -0
  48. package/src/commands/generate-entities.js +685 -66
  49. package/src/commands/generate-http-exchange.js +2 -0
  50. package/src/commands/generate-kafka-event.js +43 -10
  51. package/src/generators/base-generator.js +18 -6
  52. package/src/generators/postman-generator.js +188 -0
  53. package/src/generators/shared-generator.js +12 -2
  54. package/src/skills/build-system-yaml/SKILL.md +323 -0
  55. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +410 -0
  56. package/src/skills/build-system-yaml/references/module-spec.md +367 -0
  57. package/src/skills/build-system-yaml/references/system-yaml-spec.md +178 -0
  58. package/src/utils/config-manager.js +54 -0
  59. package/src/utils/context-builder.js +1 -0
  60. package/src/utils/domain-diagram.js +192 -0
  61. package/src/utils/domain-validator.js +1020 -0
  62. package/src/utils/fake-data.js +376 -0
  63. package/src/utils/system-validator.js +319 -199
  64. package/src/utils/yaml-to-entity.js +272 -7
  65. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  66. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  67. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +6 -5
  68. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  69. package/templates/aggregate/DomainEventHandler.java.ejs +4 -1
  70. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  71. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  72. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  73. package/templates/base/docker/Dockerfile.ejs +21 -0
  74. package/templates/base/gradle/build.gradle.ejs +3 -2
  75. package/templates/base/root/AGENTS.md.ejs +306 -45
  76. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  77. package/templates/crud/Controller.java.ejs +4 -4
  78. package/templates/crud/CreateCommand.java.ejs +4 -0
  79. package/templates/crud/CreateCommandHandler.java.ejs +3 -6
  80. package/templates/crud/CreateItemDto.java.ejs +4 -0
  81. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  82. package/templates/crud/DeleteCommandHandler.java.ejs +12 -6
  83. package/templates/crud/EndpointsController.java.ejs +6 -6
  84. package/templates/crud/FindByQuery.java.ejs +1 -1
  85. package/templates/crud/FindByQueryHandler.java.ejs +3 -9
  86. package/templates/crud/GetQueryHandler.java.ejs +2 -5
  87. package/templates/crud/ListQuery.java.ejs +1 -1
  88. package/templates/crud/ListQueryHandler.java.ejs +8 -14
  89. package/templates/crud/ScaffoldCommandHandler.java.ejs +2 -4
  90. package/templates/crud/ScaffoldQuery.java.ejs +3 -2
  91. package/templates/crud/ScaffoldQueryHandler.java.ejs +5 -6
  92. package/templates/crud/SubEntityAddCommand.java.ejs +4 -0
  93. package/templates/crud/SubEntityAddCommandHandler.java.ejs +2 -6
  94. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +2 -7
  95. package/templates/crud/TransitionCommandHandler.java.ejs +2 -6
  96. package/templates/crud/UpdateCommand.java.ejs +4 -0
  97. package/templates/crud/UpdateCommandHandler.java.ejs +2 -5
  98. package/templates/evaluate/report.html.ejs +394 -2
  99. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  100. package/templates/kafka-event/Event.java.ejs +9 -0
  101. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  102. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  103. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  104. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  105. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +25 -0
  106. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  107. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  108. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  109. package/templates/mock/MockEvent.java.ejs +10 -0
  110. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  111. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  112. package/templates/mock/SpringEventListener.java.ejs +61 -0
  113. package/templates/ports/PortDomainModel.java.ejs +35 -0
  114. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  115. package/templates/ports/PortFeignClient.java.ejs +45 -0
  116. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  117. package/templates/ports/PortInterface.java.ejs +45 -0
  118. package/templates/ports/PortNestedType.java.ejs +28 -0
  119. package/templates/ports/PortRequestDto.java.ejs +30 -0
  120. package/templates/ports/PortResponseDto.java.ejs +28 -0
  121. package/templates/postman/Collection.json.ejs +1 -1
  122. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  123. package/templates/shared/annotations/LogAfter.java.ejs +2 -0
  124. package/templates/shared/annotations/LogBefore.java.ejs +2 -0
  125. package/templates/shared/annotations/LogExceptions.java.ejs +2 -0
  126. package/templates/shared/annotations/LogLevel.java.ejs +8 -0
  127. package/templates/shared/annotations/LogTimer.java.ejs +1 -0
  128. package/templates/shared/annotations/Loggable.java.ejs +17 -0
  129. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
  130. package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +120 -32
  131. package/templates/shared/errorMessage/ErrorResponse.java.ejs +23 -0
  132. package/templates/shared/handlerException/HandlerExceptions.java.ejs +99 -79
  133. package/src/commands/generate-system.js +0 -243
  134. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +0 -1103
  135. package/templates/base/root/skill-build-domain-yaml.ejs +0 -292
  136. package/templates/base/root/skill-build-system-yaml.ejs +0 -252
  137. package/templates/shared/errorMessage/ErrorMessage.java.ejs +0 -5
  138. package/templates/shared/errorMessage/FullErrorMessage.java.ejs +0 -9
  139. package/templates/shared/errorMessage/ShortErrorMessage.java.ejs +0 -6
@@ -14,6 +14,7 @@
14
14
  - [Relaciones](#relaciones)
15
15
  - [Tipos de Datos](#tipos-de-datos)
16
16
  - [Sección endpoints](#sección-endpoints)
17
+ - [Sección listeners](#sección-listeners)
17
18
  - [Ejemplos Completos](#ejemplos-completos)
18
19
 
19
20
  ---
@@ -135,10 +136,17 @@ aggregates:
135
136
  values: []
136
137
 
137
138
  events:
138
- # Eventos de dominio que emite este agregado
139
+ # Eventos de dominio que emite este agregado (dentro del agregado)
139
140
  - name: NombreEventoOcurrido
140
141
  fields: []
141
- # kafka: true # opcional — genera publicación a Kafka vía MessageBroker
142
+
143
+ # listeners: — eventos externos que CONSUME este módulo (nivel raíz)
144
+ listeners:
145
+ - event: ExternalEvent # Nombre del evento (PascalCase + Event)
146
+ producer: other-module # Módulo que lo produce
147
+ topic: TOPIC_NAME # Topic Kafka (obligatorio en módulos standalone)
148
+ useCase: HandleExternalEvent # Caso de uso que maneja el evento
149
+ fields: [] # Payload del Integration Event recibido
142
150
  ```
143
151
 
144
152
  ### Ubicación del archivo
@@ -1280,6 +1288,100 @@ CREATE TABLE reviews (
1280
1288
 
1281
1289
  ---
1282
1290
 
1291
+ ## Soft Delete
1292
+
1293
+ Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera **eliminación lógica** en lugar de física: el registro no se borra de la base de datos, sino que se marca con un timestamp `deletedAt`.
1294
+
1295
+ ### Configuración en domain.yaml
1296
+
1297
+ ```yaml
1298
+ aggregates:
1299
+ - name: Order
1300
+ entities:
1301
+ - name: order
1302
+ isRoot: true # ← OBLIGATORIO: solo aplica en la raíz del agregado
1303
+ tableName: orders
1304
+ hasSoftDelete: true # ✅ Activa soft delete
1305
+ audit:
1306
+ enabled: true
1307
+ fields:
1308
+ - name: id
1309
+ type: String
1310
+ - name: orderNumber
1311
+ type: String
1312
+
1313
+ - name: orderItem # ← Entidad secundaria
1314
+ tableName: order_items
1315
+ # hasSoftDelete: true ← ❌ INCORRECTO — se ignora con warning
1316
+ # Las secundarias se eliminan físicamente
1317
+ # vía CASCADE desde la raíz
1318
+ ```
1319
+
1320
+ > **Regla de alcance:** `hasSoftDelete` **solo es válido en la entidad raíz** (`isRoot: true`). Ponerlo en una entidad secundaria emite un warning y es ignorado.
1321
+
1322
+ ### Por qué solo en la raíz
1323
+
1324
+ En DDD, una entidad secundaria como `OrderItem` no tiene existencia independiente — su ciclo de vida lo controla el agregado raíz. Nunca se elimina directamente: se elimina removiéndola de la colección del raíz y JPA gestiona el `DELETE` físico vía `cascade`. Aplicar `@SQLRestriction` en una entidad secundaria rompería las relaciones: los items "eliminados" desaparecerían silenciosamente del `@OneToMany`, creando inconsistencias difíciles de detectar.
1325
+
1326
+ ### Qué genera el flag
1327
+
1328
+ **Entidad de dominio:**
1329
+ ```java
1330
+ public class Order {
1331
+ private LocalDateTime deletedAt; // ← Inyectado automáticamente
1332
+
1333
+ public void softDelete() {
1334
+ if (this.deletedAt != null) {
1335
+ throw new IllegalStateException("Order is already deleted");
1336
+ }
1337
+ this.deletedAt = java.time.LocalDateTime.now();
1338
+ }
1339
+
1340
+ public boolean isDeleted() {
1341
+ return this.deletedAt != null;
1342
+ }
1343
+ }
1344
+ ```
1345
+
1346
+ **Entidad JPA:**
1347
+ ```java
1348
+ @SQLRestriction("deleted_at IS NULL") // ← Filtra automáticamente en TODAS las queries
1349
+ @Entity
1350
+ @Table(name = "orders")
1351
+ public class OrderJpa extends AuditableEntity {
1352
+ private LocalDateTime deletedAt; // ← Columna deleted_at
1353
+ }
1354
+ ```
1355
+
1356
+ **DeleteCommandHandler:**
1357
+ ```java
1358
+ // Soft delete: busca → marca → guarda (nunca deleteById)
1359
+ Order order = repository.findById(id)
1360
+ .orElseThrow(() -> new OrderNotFoundException(id));
1361
+ order.softDelete();
1362
+ repository.save(order);
1363
+ ```
1364
+
1365
+ ### Campos excluidos automáticamente
1366
+
1367
+ | Artefacto | `deletedAt` incluido | Motivo |
1368
+ |---|---|---|
1369
+ | Constructor completo (reconstrucción desde BD) | ✅ | Necesario para hidratar el estado |
1370
+ | Constructor de creación (nuevo objeto) | ❌ | Inicia siempre como `null` |
1371
+ | `CreateDto` / `CreateCommand` | ❌ | No se crea un objeto ya eliminado |
1372
+ | `ResponseDto` | ❌ | Metadato interno, no se expone en API |
1373
+ | `toJpa()` en el mapper | ✅ | Persiste el timestamp cuando se llama `softDelete()` |
1374
+
1375
+ ### Notas importantes
1376
+
1377
+ - ✅ Compatible con `audit: { enabled: true }` y `audit: { trackUser: true }` — se pueden combinar
1378
+ - ✅ `@SQLRestriction` hace invisibles los registros eliminados en **todas** las queries automáticamente, sin necesidad de filtros manuales
1379
+ - ✅ `deletedAt` **no debe** definirse manualmente en `fields` — el generador lo inyecta
1380
+ - ❌ `hasSoftDelete: true` en una entidad secundaria es ignorado con warning
1381
+ - ❌ Cuando `hasSoftDelete: true`, usar `repository.deleteById()` genera un `DeleteCommandHandler` incorrecto — el generador lo previene automáticamente
1382
+
1383
+ ---
1384
+
1283
1385
  ## Value Objects
1284
1386
 
1285
1387
  Los Value Objects son objetos inmutables que representan conceptos del dominio sin identidad propia.
@@ -1595,7 +1697,7 @@ aggregates:
1595
1697
  type: LocalDateTime
1596
1698
 
1597
1699
  - name: OrderShippedEvent
1598
- kafka: true # opcional — genera publicación a Kafka
1700
+ topic: ORDER_SHIPPED # opcional — override del topic auto-derivado
1599
1701
  fields:
1600
1702
  - name: orderId
1601
1703
  type: String
@@ -1605,11 +1707,16 @@ aggregates:
1605
1707
 
1606
1708
  ### Propiedades de un evento
1607
1709
 
1608
- | Propiedad | Tipo | Descripción |
1609
- |-----------|------|-------------|
1610
- | `name` | String | Nombre de la clase del evento (PascalCase) |
1611
- | `fields` | Array | Campos que transporta el evento |
1612
- | `kafka` | Boolean | Si `true`, genera llamada a `messageBroker.send{EventName}()` |
1710
+ | Propiedad | Tipo | Obligatorio | Descripción |
1711
+ |-----------|------|-------------|-------------|
1712
+ | `name` | String | ✅ | Nombre de la clase del evento (PascalCase) |
1713
+ | `fields` | Array | ✅ | Campos que transporta el evento |
1714
+ | `triggers` | Array\<String\> | | Nombres de métodos de transición que publican este evento. El generador emite `raise(new XEvent(...))` automáticamente. |
1715
+ | `topic` | String | ➖ | **Override del topic Kafka.** Por defecto el generador quita el sufijo `Event` del nombre de la clase: `ProductPublishedEvent` → `PRODUCT_PUBLISHED`. Úsalo cuando el consumer en `listeners[]` de otro módulo declara un topic diferente al auto-derivado. |
1716
+
1717
+ > **Regla de derivación de topic:** `ProductPublishedEvent` → quita `Event` → `ProductPublished` → SCREAMING_SNAKE → `PRODUCT_PUBLISHED`. Esto garantiza que el producer y el consumer coincidan cuando el consumer declara `topic: PRODUCT_PUBLISHED` en `listeners[]`.
1718
+
1719
+ > **`kafka: true`** ya no es necesario — si el proyecto tiene `kafka-client` instalado, todos los eventos se cablearán automáticamente al ejecutar `eva g entities`.
1613
1720
 
1614
1721
  ### Archivos generados
1615
1722
 
@@ -1644,7 +1751,7 @@ public class OrderDomainEventHandler {
1644
1751
 
1645
1752
  @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
1646
1753
  public void handle(OrderShippedEvent event) {
1647
- messageBroker.sendOrderShippedEvent(event); // kafka: true
1754
+ messageBroker.sendOrderShippedEvent(event);
1648
1755
  }
1649
1756
  }
1650
1757
  ```
@@ -1665,6 +1772,316 @@ public void confirm() {
1665
1772
 
1666
1773
  ---
1667
1774
 
1775
+ ## Sección listeners
1776
+
1777
+ La sección `listeners:` declara los eventos externos que **consume** este módulo. Es el complemento de `events:` (producción): mientras que `events:` vive _dentro_ del agregado porque pertenece al modelo de dominio, `listeners:` vive en el **nivel raíz** del `domain.yaml` porque es una responsabilidad de integración/infraestructura.
1778
+
1779
+ > **Requiere broker instalado.** El generador solo produce archivos de listener cuando `eva add kafka-client` ha sido ejecutado en el proyecto. Sin broker, la sección es ignorada.
1780
+
1781
+ ### Sintaxis
1782
+
1783
+ ```yaml
1784
+ # Nivel raíz — sibling de aggregates:
1785
+ listeners:
1786
+ - event: PaymentApprovedEvent # PascalCase + sufijo Event
1787
+ producer: payments # Módulo que produce el evento (referencia documental)
1788
+ topic: PAYMENT_APPROVED # Topic Kafka — obligatorio en módulos standalone
1789
+ useCase: ConfirmOrder # Caso de uso invocado al consumir (PascalCase)
1790
+ fields: # Payload del Integration Event recibido
1791
+ - name: orderId
1792
+ type: String
1793
+ - name: approvedAt
1794
+ type: LocalDateTime
1795
+ - name: details
1796
+ type: PaymentDetails # Campo objeto → declarado en nestedTypes:
1797
+ nestedTypes: # Opcional: records auxiliares para campos objeto
1798
+ - name: paymentDetails # camelCase → PascalCase en el record generado
1799
+ fields:
1800
+ - name: paymentId
1801
+ type: String
1802
+ - name: amount
1803
+ type: BigDecimal
1804
+ ```
1805
+
1806
+ ### Propiedades
1807
+
1808
+ | Propiedad | Requerido | Descripción |
1809
+ |-----------|-----------|-------------|
1810
+ | `event` | ✅ | Nombre del evento en PascalCase, con sufijo `Event` |
1811
+ | `producer` | ✅ | Módulo que produce el evento (solo referencia documental, no genera código) |
1812
+ | `topic` | ✅ | Topic Kafka. Si existe `system.yaml`, el generador puede inferirlo de `integrations.async[].topic`; en módulos **standalone** es obligatorio declararlo explícitamente. |
1813
+ | `useCase` | ✅ | Nombre del caso de uso que maneja el evento (PascalCase) |
1814
+ | `fields` | ✅ | Campos del payload recibido; genera el record `IntegrationEvent` y tipifica el Command despachado |
1815
+ | `nestedTypes` | ❌ | Records auxiliares para campos de tipo objeto en `fields:`. Cada entrada genera un `.java` record en `application/events/`. |
1816
+
1817
+ ### Archivos generados
1818
+
1819
+ Para cada entrada en `listeners:`, eva4j genera **5 archivos** (más un record por cada entrada en `nestedTypes:`):
1820
+
1821
+ | # | Archivo | Ubicación | Descripción |
1822
+ |---|---------|-----------|-------------|
1823
+ | 0 | `{NestedName}.java` *(por nestedType)* | `application/events/` | Record auxiliar para campos objeto |
1824
+ | 1 | `{Name}IntegrationEvent.java` | `application/events/` | Record contrato tipado (documentación + tests) |
1825
+ | 2 | `{Name}KafkaListener.java` | `infrastructure/kafkaListener/` | `@KafkaListener` — deserializa y despacha |
1826
+ | 3 | `kafka.yaml` *(todas las envs)* | `resources/parameters/*/` | Topic registrado bajo `topics:` |
1827
+ | 4 | `{UseCase}Command.java` | `application/commands/` | Comando tipado despachado desde el listener |
1828
+ | 5 | `{UseCase}CommandHandler.java` | `application/usecases/` | Stub del handler (implementar la lógica aquí) |
1829
+
1830
+ ### Código generado
1831
+
1832
+ **`PaymentDetails.java`** — `application/events/` (de `nestedTypes:`)
1833
+ ```java
1834
+ public record PaymentDetails(
1835
+ String paymentId,
1836
+ BigDecimal amount
1837
+ ) {}
1838
+ ```
1839
+
1840
+ **`PaymentApprovedIntegrationEvent.java`** — `application/events/`
1841
+ ```java
1842
+ public record PaymentApprovedIntegrationEvent(
1843
+ String orderId,
1844
+ LocalDateTime approvedAt,
1845
+ PaymentDetails details
1846
+ ) {}
1847
+ ```
1848
+
1849
+ **`ConfirmOrderCommand.java`** — `application/commands/`
1850
+ ```java
1851
+ import com.example.orders.application.events.PaymentDetails;
1852
+
1853
+ public record ConfirmOrderCommand(
1854
+ String orderId,
1855
+ LocalDateTime approvedAt,
1856
+ PaymentDetails details
1857
+ ) implements Command {}
1858
+ ```
1859
+
1860
+ **`PaymentApprovedKafkaListener.java`** — `infrastructure/kafkaListener/`
1861
+ ```java
1862
+ import com.example.orders.application.events.PaymentDetails;
1863
+
1864
+ @Component
1865
+ public class PaymentApprovedKafkaListener {
1866
+
1867
+ private final UseCaseMediator useCaseMediator;
1868
+ private final ObjectMapper objectMapper;
1869
+
1870
+ @Value("${topics.payment-approved}")
1871
+ private String paymentApprovedTopic;
1872
+
1873
+ public PaymentApprovedKafkaListener(UseCaseMediator useCaseMediator,
1874
+ ObjectMapper objectMapper) {
1875
+ this.useCaseMediator = useCaseMediator;
1876
+ this.objectMapper = objectMapper;
1877
+ }
1878
+
1879
+ @KafkaListener(topics = "${topics.payment-approved}")
1880
+ public void handle(EventEnvelope<Map<String, Object>> event, Acknowledgment ack) {
1881
+ String orderId = objectMapper.convertValue(event.data().get("orderId"), String.class);
1882
+ LocalDateTime approvedAt = objectMapper.convertValue(
1883
+ event.data().get("approvedAt"), LocalDateTime.class);
1884
+ PaymentDetails details = objectMapper.convertValue(
1885
+ event.data().get("details"), PaymentDetails.class);
1886
+ useCaseMediator.dispatch(new ConfirmOrderCommand(orderId, approvedAt, details));
1887
+ ack.acknowledge();
1888
+ }
1889
+ }
1890
+ ```
1891
+
1892
+ **`ConfirmOrderCommandHandler.java`** — `application/usecases/`
1893
+ ```java
1894
+ @ApplicationComponent
1895
+ public class ConfirmOrderCommandHandler implements CommandHandler<ConfirmOrderCommand> {
1896
+
1897
+ @Override
1898
+ public void handle(ConfirmOrderCommand command) {
1899
+ // TODO: implement ConfirmOrder business logic
1900
+ throw new UnsupportedOperationException("ConfirmOrderCommandHandler not yet implemented");
1901
+ }
1902
+ }
1903
+ ```
1904
+
1905
+ ### Deserialización — cómo funciona
1906
+
1907
+ El consumidor Kafka recibe un `EventEnvelope<Map<String, Object>>`. El listener generado extrae cada campo con `objectMapper.convertValue()`, que maneja:
1908
+
1909
+ - Primitivos y strings: `convertValue(map.get("field"), String.class)`
1910
+ - Fechas: `convertValue(map.get("date"), LocalDateTime.class)` — requiere el módulo Jackson JavaTime
1911
+ - Objetos / nested types: `convertValue(map.get("details"), PaymentDetails.class)` — funciona automáticamente para Java records
1912
+ - Listas: `convertValue(map.get("items"), typeFactory.constructCollectionType(List.class, ItemType.class))`
1913
+
1914
+ ### `nestedTypes:` — cuándo usarlo
1915
+
1916
+ Usar `nestedTypes:` cuando uno de los `fields:` es un objeto estructurado (no un tipo Java primitivo). Cada entrada genera un Java record en `application/events/` que se importa automáticamente tanto en el `KafkaListener` como en el `Command`.
1917
+
1918
+ El nombre se declara en `camelCase` y el generador lo normaliza a `PascalCase`:
1919
+ ```yaml
1920
+ nestedTypes:
1921
+ - name: paymentDetails # → PaymentDetails.java
1922
+ fields:
1923
+ - name: paymentId
1924
+ type: String
1925
+ - name: amount
1926
+ type: BigDecimal
1927
+ ```
1928
+
1929
+ `{Name}IntegrationEvent.java` **no necesita importar** los nested types porque vive en el mismo paquete `application/events/`.
1930
+
1931
+ ### Regla de resolución de `topic:`
1932
+
1933
+ | Escenario | Comportamiento |
1934
+ |-----------|---------------|
1935
+ | Módulo standalone (solo `domain.yaml`) | `topic:` **obligatorio** — no hay otra fuente de verdad |
1936
+ | Proyecto con `system.yaml` | `topic:` puede omitirse; se infiere de `integrations.async[].topic` |
1937
+ | `topic:` declarado explícitamente con `system.yaml` | El valor declarado tiene **precedencia** sobre la inferencia |
1938
+
1939
+ ### Contraste: producción vs. consumo
1940
+
1941
+ ```
1942
+ domain.yaml
1943
+ ├── aggregates:
1944
+ │ └── [Aggregate]
1945
+ │ └── events: → Domain Events que PRODUCE (domain/models/events/)
1946
+
1947
+ ├── listeners: → Integration Events que CONSUME (infrastructure/kafkaListener/)
1948
+
1949
+ └── ports: → Servicios HTTP que LLAMA (infrastructure/adapters/{service}/)
1950
+ ```
1951
+
1952
+ ---
1953
+
1954
+ ## Sección ports
1955
+
1956
+ `ports:` es una sección de nivel raíz (sibling de `aggregates:` y `listeners:`) que declara los servicios HTTP síncronos que el módulo llama, implementados con Spring Cloud OpenFeign.
1957
+
1958
+ **Regla de agrupación:** una **entrada** = un **método**. Entradas que comparten el mismo `service:` se generan en un único FeignClient.
1959
+
1960
+ ### Propiedades de una entrada en `ports[]`
1961
+
1962
+ | Propiedad | Tipo | Obligatorio | Descripción |
1963
+ |-----------|------|-------------|-------------|
1964
+ | `name` | String | ✅ | Nombre del método (camelCase) |
1965
+ | `service` | String | ✅ | Nombre del servicio/interfaz (PascalCase). Agrupa métodos en un FeignClient |
1966
+ | `target` | String | ❌ | Módulo destino — referencia documental; no afecta la generación |
1967
+ | `baseUrl` | String | ❌* | URL base del servicio. Declarar solo en la **primera entrada** del `service:` |
1968
+ | `http` | String | ✅ | Verbo HTTP + path: `GET /resource/{id}`, `POST /resource`, etc. |
1969
+ | `fields` | Array | ❌ | Campos de la respuesta → genera `{MethodPascal}ResponseDto.java` |
1970
+ | `body` | Array | ❌ | Campos del cuerpo de la petición (solo POST/PUT/PATCH) → genera `{MethodPascal}RequestDto.java` |
1971
+ | `returnList` | Boolean | ❌ | `true` → retorno `List<{MethodPascal}ResponseDto>`. Default: `false` |
1972
+ | `nestedTypes` | Array | ❌ | Records auxiliares para campos de tipo objeto en `body:` o `fields:` |
1973
+
1974
+ *Si se omite `baseUrl` en todas las entradas de un `service:`, se emite un warning y se usa `http://localhost:8080`.
1975
+
1976
+ ### Reglas de uso
1977
+
1978
+ - **`baseUrl:`** — declarar únicamente en la primera entrada del `service:`. Eva4j la registra en `parameters/{env}/urls.yaml` como `{module-kebab}.{service-kebab}.base-url`.
1979
+ - **`body:`** — válido solo en POST, PUT y PATCH. En GET/DELETE emite warning y se ignora.
1980
+ - **`returnList: true`** — el tipo de retorno pasa de `{Method}ResponseDto` a `List<{Method}ResponseDto>`.
1981
+ - **`fields:` omitido** → retorno `void` en la interfaz del puerto y en el FeignClient.
1982
+ - **`nestedTypes:`** — se generan como records separados en `application/dtos/`. Se deduplican por nombre dentro del mismo `service:`. Aplica tanto para campos en `body:` como en `fields:`.
1983
+
1984
+ ### Estructura mínima
1985
+
1986
+ ```yaml
1987
+ # Un solo método GET, respuesta simple
1988
+ ports:
1989
+ - name: findScreeningById
1990
+ service: ScreeningService
1991
+ target: screenings
1992
+ baseUrl: http://localhost:8081
1993
+ http: GET /screenings/{id}
1994
+ fields:
1995
+ - name: id
1996
+ type: String
1997
+ - name: startTime
1998
+ type: LocalDateTime
1999
+ ```
2000
+
2001
+ ### Ejemplo con todos los patrones
2002
+
2003
+ ```yaml
2004
+ ports:
2005
+ # GET con variable de ruta
2006
+ - name: findScreeningById
2007
+ service: ScreeningService
2008
+ target: screenings
2009
+ baseUrl: http://localhost:8081 # ← solo en la primera entrada del service
2010
+ http: GET /screenings/{id}
2011
+ fields:
2012
+ - name: id
2013
+ type: String
2014
+ - name: startTime
2015
+ type: LocalDateTime
2016
+
2017
+ # GET retornando lista
2018
+ - name: findAvailableSeats
2019
+ service: ScreeningService # mismo service → mismo FeignClient
2020
+ target: screenings
2021
+ http: GET /screenings/{id}/seats
2022
+ returnList: true # → List<FindAvailableSeatResponseDto>
2023
+ fields:
2024
+ - name: seatId
2025
+ type: String
2026
+ - name: seatType
2027
+ type: String
2028
+
2029
+ # POST con body + nestedType + respuesta
2030
+ - name: processPayment
2031
+ service: PaymentGateway
2032
+ target: payment-gateway-external
2033
+ baseUrl: https://api.payments.example.com
2034
+ http: POST /payments
2035
+ body:
2036
+ - name: amount
2037
+ type: BigDecimal
2038
+ - name: paymentMethod
2039
+ type: PaymentMethodInput # tipo objeto → declarar en nestedTypes:
2040
+ nestedTypes:
2041
+ - name: paymentMethodInput
2042
+ fields:
2043
+ - name: type
2044
+ type: String
2045
+ - name: cardToken
2046
+ type: String
2047
+ fields:
2048
+ - name: paymentId
2049
+ type: String
2050
+ - name: status
2051
+ type: String
2052
+
2053
+ # DELETE void (sin fields)
2054
+ - name: cancelPayment
2055
+ service: PaymentGateway
2056
+ target: payment-gateway-external
2057
+ http: DELETE /payments/{id}
2058
+ # fields: omitido → retorno void
2059
+ ```
2060
+
2061
+ ### Artefactos generados por `service:` único
2062
+
2063
+ | Archivo | Descripción |
2064
+ |---------|-------------|
2065
+ | `domain/repositories/{ServiceName}.java` | Interfaz del puerto secundario |
2066
+ | `infrastructure/adapters/{service}/{ServiceName}FeignClient.java` | `@FeignClient` tipado |
2067
+ | `infrastructure/adapters/{service}/{ServiceName}FeignAdapter.java` | `@Component implements {ServiceName}` |
2068
+ | `infrastructure/adapters/{service}/{ServiceName}FeignConfig.java` | Configuración de timeouts |
2069
+ | `parameters/{env}/urls.yaml` | Propiedad `{module}.{service}.base-url` |
2070
+
2071
+ ### Artefactos generados por método
2072
+
2073
+ | Archivo | Condición |
2074
+ |---------|-----------|
2075
+ | `application/dtos/{MethodPascal}ResponseDto.java` | Cuando `fields:` tiene elementos |
2076
+ | `application/dtos/{MethodPascal}RequestDto.java` | Cuando `body:` tiene elementos (POST/PUT/PATCH) |
2077
+ | `application/dtos/{NestedTypePascal}.java` | Por cada entrada en `nestedTypes:` |
2078
+
2079
+ ### Referencia
2080
+
2081
+ Ver ejemplo completo en [`examples/domain-ports.yaml`](examples/domain-ports.yaml).
2082
+
2083
+ ---
2084
+
1668
2085
  ## Relaciones
1669
2086
 
1670
2087
  eva4j soporta relaciones JPA bidireccionales completas con generación automática del lado inverso.
@@ -2494,7 +2911,7 @@ El tipo del use case se infiere automáticamente del método HTTP:
2494
2911
 
2495
2912
  | Categoría | Nombres Match | Generado |
2496
2913
  |-----------|---------------|---------|
2497
- | **Estándar** | `Create{Aggregate}`, `Update{Aggregate}`, `Delete{Aggregate}`, `Get{Aggregate}`, `FindAll{Aggregate}s` | Implementación completa con lógica de repositorio |
2914
+ | **Estándar** | `Create{Aggregate}`, `Update{Aggregate}`, `Delete{Aggregate}`, `Get{Aggregate}`, `FindAll{PluralAggregate}` | Implementación completa con lógica de repositorio |
2498
2915
  | **Scaffold** | Cualquier otro nombre (`ConfirmOrder`, `ActivateProduct`, etc.) | Clase con `// TODO` — el desarrollador completa la lógica |
2499
2916
 
2500
2917
  Los use cases estándar reutilizan los templates CRUD existentes (implementación idéntica al flujo sin `endpoints:`). Los scaffolds generan archivos con `UnsupportedOperationException` y comentarios guía.
@@ -2568,14 +2985,13 @@ application/
2568
2985
  ConfirmOrderCommand.java ← scaffold (TODO)
2569
2986
  queries/
2570
2987
  GetOrderQuery.java ← estándar (completo)
2571
- FindAllOrdersQuery.java ← NOTA: no es estándar (estándar sería FindAllOrders s)
2572
- → scaffold (TODO)
2988
+ FindAllOrdersQuery.java ← estándar (completo)
2573
2989
  usecases/
2574
2990
  CreateOrderCommandHandler.java ← estándar
2575
2991
  DeleteOrderCommandHandler.java ← estándar
2576
2992
  ConfirmOrderCommandHandler.java ← scaffold
2577
2993
  GetOrderQueryHandler.java ← estándar
2578
- FindAllOrdersQueryHandler.java ← scaffold
2994
+ FindAllOrdersQueryHandler.java ← estándar
2579
2995
  dtos/
2580
2996
  OrderResponseDto.java
2581
2997
  mappers/
@@ -3010,7 +3426,7 @@ eva4j generate entities <module-name>
3010
3426
  - Control de visibilidad de campos (`readOnly`, `hidden`, `defaultValue`)
3011
3427
  - Referencias cross-agregado (`reference:`)
3012
3428
  - Domain Events (`events:` con soporte opcional de Kafka)
3013
- - Soft delete a nivel de módulo (configurado en `eva add module`)
3429
+ - Soft delete por entidad raíz (`hasSoftDelete: true` en `isRoot: true`) ✅ Implementado
3014
3430
 
3015
3431
  ### 🚧 Próximamente
3016
3432