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
@@ -13,6 +13,8 @@
13
13
  - [Validaciones JSR-303](#validaciones-jsr-303)
14
14
  - [Relaciones](#relaciones)
15
15
  - [Tipos de Datos](#tipos-de-datos)
16
+ - [Sección endpoints](#sección-endpoints)
17
+ - [Sección listeners](#sección-listeners)
16
18
  - [Ejemplos Completos](#ejemplos-completos)
17
19
 
18
20
  ---
@@ -134,10 +136,17 @@ aggregates:
134
136
  values: []
135
137
 
136
138
  events:
137
- # Eventos de dominio que emite este agregado
139
+ # Eventos de dominio que emite este agregado (dentro del agregado)
138
140
  - name: NombreEventoOcurrido
139
141
  fields: []
140
- # 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
141
150
  ```
142
151
 
143
152
  ### Ubicación del archivo
@@ -1279,6 +1288,100 @@ CREATE TABLE reviews (
1279
1288
 
1280
1289
  ---
1281
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
+
1282
1385
  ## Value Objects
1283
1386
 
1284
1387
  Los Value Objects son objetos inmutables que representan conceptos del dominio sin identidad propia.
@@ -1594,7 +1697,7 @@ aggregates:
1594
1697
  type: LocalDateTime
1595
1698
 
1596
1699
  - name: OrderShippedEvent
1597
- kafka: true # opcional — genera publicación a Kafka
1700
+ topic: ORDER_SHIPPED # opcional — override del topic auto-derivado
1598
1701
  fields:
1599
1702
  - name: orderId
1600
1703
  type: String
@@ -1604,11 +1707,16 @@ aggregates:
1604
1707
 
1605
1708
  ### Propiedades de un evento
1606
1709
 
1607
- | Propiedad | Tipo | Descripción |
1608
- |-----------|------|-------------|
1609
- | `name` | String | Nombre de la clase del evento (PascalCase) |
1610
- | `fields` | Array | Campos que transporta el evento |
1611
- | `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`.
1612
1720
 
1613
1721
  ### Archivos generados
1614
1722
 
@@ -1643,7 +1751,7 @@ public class OrderDomainEventHandler {
1643
1751
 
1644
1752
  @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
1645
1753
  public void handle(OrderShippedEvent event) {
1646
- messageBroker.sendOrderShippedEvent(event); // kafka: true
1754
+ messageBroker.sendOrderShippedEvent(event);
1647
1755
  }
1648
1756
  }
1649
1757
  ```
@@ -1664,6 +1772,316 @@ public void confirm() {
1664
1772
 
1665
1773
  ---
1666
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
+
1667
2085
  ## Relaciones
1668
2086
 
1669
2087
  eva4j soporta relaciones JPA bidireccionales completas con generación automática del lado inverso.
@@ -2441,6 +2859,154 @@ private List<AddressJpa> addresses = new ArrayList<>();
2441
2859
 
2442
2860
  ---
2443
2861
 
2862
+ ## Sección endpoints
2863
+
2864
+ La sección `endpoints:` es **opcional** y se declara como clave hermana de `aggregates:` en el YAML. Cuando está presente, controla **qué use cases y controladores REST se generan**. Cuando está ausente, el generador usa el flujo interactivo tradicional (5 CRUD fijos por aggregate root).
2865
+
2866
+ ### Comportamiento condicional
2867
+
2868
+ | Condición | Comportamiento |
2869
+ |-----------|---------------|
2870
+ | `endpoints:` **ausente** | Pregunta interactiva "¿Generar CRUD?" → genera 5 use cases estándar |
2871
+ | `endpoints:` **presente** | Genera automáticamente solo los use cases declarados en `operations[]` |
2872
+
2873
+ ### Sintaxis
2874
+
2875
+ ```yaml
2876
+ # Sección endpoints: sibling de aggregates:
2877
+ endpoints:
2878
+ basePath: /orders # Ruta base (incluida en @RequestMapping "/api/{version}{basePath}")
2879
+ versions:
2880
+ - version: v1 # Versión del API (ej: v1, v2, v1-beta)
2881
+ operations:
2882
+ - method: GET # HTTP method (GET, POST, PUT, PATCH, DELETE)
2883
+ path: /{id} # Path relativo al basePath (/ para la raíz)
2884
+ useCase: GetOrder # Nombre del use case (PascalCase)
2885
+ description: "Obtener pedido por ID" # Descripción para Swagger
2886
+ ```
2887
+
2888
+ ### Campos de `endpoints:`
2889
+
2890
+ | Campo | Tipo | Requerido | Descripción |
2891
+ |-------|------|-----------|-------------|
2892
+ | `basePath` | String | Sí | Ruta base del recurso (ej: `/orders`) |
2893
+ | `versions` | Array | Sí | Lista de versiones de API |
2894
+ | `versions[].version` | String | Sí | Identificador de versión (ej: `v1`) |
2895
+ | `versions[].operations` | Array | Sí | Lista de endpoints a generar |
2896
+ | `operations[].method` | String | Sí | Verbo HTTP: `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
2897
+ | `operations[].path` | String | Sí | Path relativo (ej: `/`, `/{id}`, `/{id}/confirm`) |
2898
+ | `operations[].useCase` | String | Sí | Nombre del use case en PascalCase |
2899
+ | `operations[].description` | String | No | Descripción para la anotación `@Operation` de Swagger |
2900
+
2901
+ ### Tipo inferido (`type`)
2902
+
2903
+ El tipo del use case se infiere automáticamente del método HTTP:
2904
+
2905
+ | HTTP method | Tipo inferido | Genera |
2906
+ |-------------|--------------|--------|
2907
+ | `GET` | `query` | `{UseCaseName}Query` + `{UseCaseName}QueryHandler` |
2908
+ | `POST`, `PUT`, `PATCH`, `DELETE` | `command` | `{UseCaseName}Command` + `{UseCaseName}CommandHandler` |
2909
+
2910
+ ### Use cases estándar vs. scaffold
2911
+
2912
+ | Categoría | Nombres Match | Generado |
2913
+ |-----------|---------------|---------|
2914
+ | **Estándar** | `Create{Aggregate}`, `Update{Aggregate}`, `Delete{Aggregate}`, `Get{Aggregate}`, `FindAll{PluralAggregate}` | Implementación completa con lógica de repositorio |
2915
+ | **Scaffold** | Cualquier otro nombre (`ConfirmOrder`, `ActivateProduct`, etc.) | Clase con `// TODO` — el desarrollador completa la lógica |
2916
+
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.
2918
+
2919
+ ### Regla anti-duplicado (multi-versión)
2920
+
2921
+ Cuando el mismo `useCase` aparece en múltiples versiones (ej: `CreateProduct` en v1 y v2), el generador crea el Command/Query + Handler **solo una vez** (en la primera versión donde aparece). Los controladores de las versiones posteriores importan y referencian el mismo use case sin regenerarlo.
2922
+
2923
+ ```yaml
2924
+ endpoints:
2925
+ basePath: /products
2926
+ versions:
2927
+ - version: v1
2928
+ operations:
2929
+ - { method: POST, path: /, useCase: CreateProduct } # ← genera CreateProductCommand + Handler
2930
+
2931
+ - version: v2
2932
+ operations:
2933
+ - { method: POST, path: /, useCase: CreateProduct } # ← NO regenera, solo referencia en V2Controller
2934
+ - { method: PUT, path: /{id}/activate, useCase: ActivateProduct } # ← nuevo scaffold
2935
+ ```
2936
+
2937
+ ### Nombres de controladores generados
2938
+
2939
+ Con `endpoints:`, el controlador se nombra `{Aggregate}{VersionCapitalized}Controller`:
2940
+
2941
+ | Aggregate | Version | Clase generada | Archivo |
2942
+ |-----------|---------|---------------|---------|
2943
+ | `Order` | `v1` | `OrderV1Controller` | `controllers/order/v1/OrderV1Controller.java` |
2944
+ | `Product` | `v2` | `ProductV2Controller` | `controllers/product/v2/ProductV2Controller.java` |
2945
+
2946
+ > Sin `endpoints:`, el controlador se llama `{Aggregate}Controller` y usa la versión ingresada en el prompt.
2947
+
2948
+ ### Ejemplo básico (una versión)
2949
+
2950
+ ```yaml
2951
+ aggregates:
2952
+ - name: Order
2953
+ entities:
2954
+ - name: order
2955
+ isRoot: true
2956
+ tableName: orders
2957
+ fields:
2958
+ - { name: id, type: String }
2959
+ - { name: orderNumber, type: String }
2960
+ - { name: status, type: OrderStatus, readOnly: true }
2961
+
2962
+ enums:
2963
+ - name: OrderStatus
2964
+ initialValue: PENDING
2965
+ values: [PENDING, CONFIRMED, SHIPPED, CANCELLED]
2966
+
2967
+ endpoints:
2968
+ basePath: /orders
2969
+ versions:
2970
+ - version: v1
2971
+ operations:
2972
+ - { method: GET, path: /{id}, useCase: GetOrder, description: "Obtener pedido" }
2973
+ - { method: GET, path: /, useCase: FindAllOrders, description: "Listar pedidos" }
2974
+ - { method: POST, path: /, useCase: CreateOrder, description: "Crear pedido" }
2975
+ - { method: DELETE, path: /{id}, useCase: DeleteOrder, description: "Eliminar pedido" }
2976
+ - { method: PUT, path: /{id}/confirm, useCase: ConfirmOrder, description: "Confirmar pedido" }
2977
+ ```
2978
+
2979
+ **Archivos generados:**
2980
+ ```
2981
+ application/
2982
+ commands/
2983
+ CreateOrderCommand.java ← estándar (completo)
2984
+ DeleteOrderCommand.java ← estándar (completo)
2985
+ ConfirmOrderCommand.java ← scaffold (TODO)
2986
+ queries/
2987
+ GetOrderQuery.java ← estándar (completo)
2988
+ FindAllOrdersQuery.java ← estándar (completo)
2989
+ usecases/
2990
+ CreateOrderCommandHandler.java ← estándar
2991
+ DeleteOrderCommandHandler.java ← estándar
2992
+ ConfirmOrderCommandHandler.java ← scaffold
2993
+ GetOrderQueryHandler.java ← estándar
2994
+ FindAllOrdersQueryHandler.java ← estándar
2995
+ dtos/
2996
+ OrderResponseDto.java
2997
+ mappers/
2998
+ OrderApplicationMapper.java
2999
+ infrastructure/rest/controllers/order/
3000
+ v1/
3001
+ OrderV1Controller.java ← controller con 5 métodos declarados
3002
+ ```
3003
+
3004
+ ### Ejemplo multi-versión
3005
+
3006
+ Ver [`examples/domain-endpoints-versioned.yaml`](examples/domain-endpoints-versioned.yaml) para un ejemplo completo con v1 y v2, incluyendo la regla anti-duplicado y scaffolds.
3007
+
3008
+ ---
3009
+
2444
3010
  ## Ejemplos Completos
2445
3011
 
2446
3012
  ### Ejemplo 1: E-Commerce (Order)
@@ -2860,7 +3426,7 @@ eva4j generate entities <module-name>
2860
3426
  - Control de visibilidad de campos (`readOnly`, `hidden`, `defaultValue`)
2861
3427
  - Referencias cross-agregado (`reference:`)
2862
3428
  - Domain Events (`events:` con soporte opcional de Kafka)
2863
- - 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
2864
3430
 
2865
3431
  ### 🚧 Próximamente
2866
3432