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
@@ -0,0 +1,46 @@
1
+ package <%= packageName %>.<%= moduleName %>.domain.models.events;
2
+ <%
3
+ const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal');
4
+ const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType));
5
+ const needsInstant = fields && fields.some(f => f.javaType === 'Instant');
6
+ const needsUUID = fields && fields.some(f => f.javaType === 'UUID');
7
+ const needsList = fields && fields.some(f => f.javaType && f.javaType.startsWith('List'));
8
+ %>
9
+ <% if (needsBigDecimal) { %>
10
+ import java.math.BigDecimal;
11
+ <% } %>
12
+ <% if (needsLocalDate) { %>
13
+ import java.time.LocalDate;
14
+ import java.time.LocalDateTime;
15
+ <% } %>
16
+ <% if (needsInstant) { %>
17
+ import java.time.Instant;
18
+ <% } %>
19
+ <% if (needsUUID) { %>
20
+ import java.util.UUID;
21
+ <% } %>
22
+ <% if (needsList) { %>
23
+ import java.util.List;
24
+ <% } %>
25
+
26
+ /**
27
+ * <%= name %> — snapshot value type carried in domain events.
28
+ *
29
+ * This is a pure domain record with no infrastructure dependencies.
30
+ * TODO: Define the fields this snapshot should carry.
31
+ * Example for an order item:
32
+ * String productId,
33
+ * String productName,
34
+ * Integer quantity,
35
+ * BigDecimal unitPrice
36
+ */
37
+ public record <%= name %>(
38
+ <% if (fields && fields.length > 0) { %>
39
+ <% fields.forEach((field, idx) => { %>
40
+ <%- field.javaType %> <%= field.name %><%= idx < fields.length - 1 ? ',' : '' %>
41
+ <% }); %>
42
+ <% } else { %>
43
+ // TODO: Add the fields this snapshot should carry
44
+ <% } %>
45
+ ) {
46
+ }
@@ -22,11 +22,17 @@ import <%= packageName %>.shared.domain.AuditableEntity;
22
22
  <% } else if (auditable) { %>
23
23
  import <%= packageName %>.shared.domain.AuditableEntity;
24
24
  <% } %>
25
+ <% if (hasSoftDelete) { %>
26
+ import org.hibernate.annotations.SQLRestriction;
27
+ <% } %>
25
28
 
26
29
  /**
27
30
  * <%= name %>Jpa - JPA Entity
28
31
  * Persistence entity with JPA annotations
29
32
  */
33
+ <% if (hasSoftDelete) { %>
34
+ @SQLRestriction("deleted_at IS NULL")
35
+ <% } %>
30
36
  @Entity
31
37
  @Table(name = "<%= tableName %>")
32
38
  @Getter
@@ -8,4 +8,9 @@ import <%= packageName %>.<%= moduleName %>.infrastructure.database.entities.<%=
8
8
  * Spring Data JPA repository
9
9
  */
10
10
  public interface <%= rootEntity.name %>JpaRepository extends JpaRepository<<%= rootEntity.name %>Jpa, <%= rootEntity.fields[0].javaType %>> {
11
+ <% if (findByOps && findByOps.length > 0) { %>
12
+ <% findByOps.forEach(function(op) { %>
13
+ org.springframework.data.domain.Page<<%= rootEntity.name %>Jpa> <%= op.jpaMethodName %>(<%= op.fieldJavaType %> <%= op.fieldName %>, org.springframework.data.domain.Pageable pageable);
14
+ <% }); %>
15
+ <% } %>
11
16
  }
@@ -50,14 +50,15 @@ dependencies {
50
50
  <% } %><% if (dependencies.includes('security')) { %> implementation 'org.springframework.boot:spring-boot-starter-security'
51
51
  <% } %><% if (dependencies.includes('validation')) { %> implementation 'org.springframework.boot:spring-boot-starter-validation'
52
52
  <% } %><% if (dependencies.includes('actuator')) { %> implementation 'org.springframework.boot:spring-boot-starter-aop'
53
- <% } %><% if (features.includeDevtools) { %> developmentOnly 'org.springframework.boot:spring-boot-devtools'
53
+ <% } %> implementation 'org.springframework.boot:spring-boot-starter-actuator'
54
+ <% if (features.includeDevtools) { %> developmentOnly 'org.springframework.boot:spring-boot-devtools'
54
55
  <% } %><% if (features.includeLombok) { %>
55
56
  compileOnly 'org.projectlombok:lombok'
56
57
  annotationProcessor 'org.projectlombok:lombok'
57
58
  <% } %><% if (dependencies.includes('data-jpa')) { %>
58
59
  runtimeOnly '<%= databaseDriver %>'
59
60
  <% } %><% if (features.includeSwagger) { %>
60
- implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
61
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:<%= springdocVersion %>'
61
62
  <% } %>
62
63
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
63
64
  <% if (dependencies.includes('security')) { %> testImplementation 'org.springframework.security:spring-security-test'
@@ -328,13 +328,21 @@ aggregates:
328
328
  - CANCELLED
329
329
 
330
330
  events: # Domain Events emitted by this aggregate
331
- - name: OrderConfirmedEvent
332
- fields:
333
- - name: orderId
331
+ - name: OrderConfirmedEvent # Nota: kafka: true ya NO es necesario.
332
+ fields: # Si hay kafka-client instalado, eva g entities
333
+ - name: orderId # cableará automáticamente todos los eventos.
334
334
  type: String
335
335
  - name: confirmedAt
336
336
  type: LocalDateTime
337
- # kafka: true # Optional — publishes to Kafka via MessageBroker
337
+
338
+ # listeners: # Sibling de aggregates: — eventos externos que CONSUME
339
+ # - event: PaymentApprovedEvent
340
+ # producer: payments
341
+ # topic: PAYMENT_APPROVED # obligatorio en módulos standalone
342
+ # useCase: ConfirmOrder
343
+ # fields:
344
+ # - name: orderId
345
+ # type: String
338
346
  ```
339
347
 
340
348
  ### Supported Field Types
@@ -1525,6 +1533,8 @@ public boolean canConfirm() { return this.status.canTransitionTo(OrderStatus.CO
1525
1533
 
1526
1534
  ### Domain Events (`events[]`)
1527
1535
 
1536
+ Los eventos se declaran bajo el agregado (al mismo nivel que `entities:`, `enums:`, `valueObjects:`). Opcionalmente, se conectan con transiciones de estado mediante la propiedad `triggers`.
1537
+
1528
1538
  ```yaml
1529
1539
  aggregates:
1530
1540
  - name: Order
@@ -1532,64 +1542,301 @@ aggregates:
1532
1542
  - name: order
1533
1543
  isRoot: true
1534
1544
  # ...
1545
+ enums:
1546
+ - name: OrderStatus
1547
+ initialValue: DRAFT
1548
+ transitions:
1549
+ - from: DRAFT
1550
+ to: PLACED
1551
+ method: place
1552
+ - from: PLACED
1553
+ to: CANCELLED
1554
+ method: cancel
1555
+ values: [DRAFT, PLACED, CANCELLED]
1535
1556
  events:
1536
- - name: OrderConfirmedEvent
1557
+ - name: OrderPlaced
1558
+ topic: ORDER_PLACED # opcional: sobreescribe el topic auto-derivado
1559
+ triggers:
1560
+ - place # ← nombre del método de transición
1537
1561
  fields:
1538
- - name: orderId
1562
+ - name: orderId # declarar para consumidores cross-módulo via Kafka
1539
1563
  type: String
1540
- - name: confirmedAt
1564
+ - name: customerId
1565
+ type: String
1566
+ - name: totalAmount
1567
+ type: BigDecimal
1568
+ - name: placedAt
1541
1569
  type: LocalDateTime
1542
- # kafka: true # Optional — generates messageBroker.sendOrderConfirmedEvent()
1543
-
1544
- - name: OrderShippedEvent
1545
- kafka: true # Publishes to Kafka after commit
1570
+ - name: OrderCancelled
1571
+ triggers:
1572
+ - cancel
1546
1573
  fields:
1547
- - name: orderId
1548
- type: String
1549
- - name: trackingNumber
1574
+ - name: reason # no resuelto → null /* TODO: provide reason */
1550
1575
  type: String
1551
1576
  ```
1552
1577
 
1553
- **Archivos generados:**
1578
+ #### Propiedad `topic` (opcional)
1554
1579
 
1555
- `OrderConfirmedEvent.java` en `domain/models/events/`:
1556
- ```java
1557
- public final class OrderConfirmedEvent extends DomainEvent {
1558
- private final String orderId;
1559
- private final LocalDateTime confirmedAt;
1560
-
1561
- public OrderConfirmedEvent(String aggregateId, String orderId, LocalDateTime confirmedAt) {
1562
- super(aggregateId);
1563
- this.orderId = orderId;
1564
- this.confirmedAt = confirmedAt;
1565
- }
1566
- // getters
1567
- }
1580
+ Sobreescribe el nombre del topic Kafka auto-derivado para este evento.
1581
+
1582
+ **Regla de derivación por defecto:** el generador quita el sufijo `Event` del nombre de la clase antes de convertir a SCREAMING_SNAKE_CASE:
1583
+ - `ProductPublishedEvent` `PRODUCT_PUBLISHED` ✓ (no `PRODUCT_PUBLISHED_EVENT`)
1584
+ - `OrderCancelled` `ORDER_CANCELLED` (sin sufijo, sin cambios)
1585
+
1586
+ **Cuándo usar `topic:` explícito:**
1587
+ - El producer y el consumer de otro módulo deben usar exactamente el mismo nombre.
1588
+ - Si el consumer en `listeners[]` declara `topic: MY_CUSTOM_TOPIC`, declara el mismo valor aquí para que el match sea garantizado.
1589
+
1590
+ ```yaml
1591
+ events:
1592
+ - name: ProductPublishedEvent
1593
+ # topic auto-derivado: PRODUCT_PUBLISHED (sufijo 'Event' eliminado)
1594
+ triggers: [publish]
1595
+ fields: [...]
1596
+
1597
+ - name: OrderReadyEvent
1598
+ topic: ORDER_READY_FOR_PICKUP # override explícito
1599
+ triggers: [markReady]
1600
+ fields: [...]
1568
1601
  ```
1569
1602
 
1570
- `OrderDomainEventHandler.java` — en `application/usecases/`:
1571
- ```java
1572
- @Component
1573
- public class OrderDomainEventHandler {
1603
+ > **Nota:** el flag `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`.
1574
1604
 
1575
- @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
1576
- public void handle(OrderConfirmedEvent event) { /* lógica post-commit */ }
1605
+ #### Propiedad `triggers`
1577
1606
 
1578
- @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
1579
- public void handle(OrderShippedEvent event) {
1580
- messageBroker.sendOrderShippedEvent(event); // kafka: true
1581
- }
1607
+ Lista de nombres de métodos de transición que publican este evento. El generador emite automáticamente `raise(new XEvent(...))` dentro de cada método listado.
1608
+
1609
+ **Reglas de resolución de argumentos (en orden):**
1610
+
1611
+ | Condición del campo del evento | Argumento generado |
1612
+ |---|---|
1613
+ | Siempre (primer arg, `aggregateId` del `DomainEvent` base) | `this.getId()` |
1614
+ | Nombre = `{entityName}Id` (ej: `orderId` en `Order`) | **Ignorado** en el Domain Event class — mapeado a `event.getAggregateId()` en el Integration Event |
1615
+ | Nombre coincide con un campo de la entidad | `this.get{Field}()` |
1616
+ | Nombre termina en `At` + tipo `LocalDateTime` | `LocalDateTime.now()` |
1617
+ | No resuelto | `null /* TODO: provide {fieldName} */` |
1618
+
1619
+ > **Convención:** Sí declarar `{entityName}Id` en `events[].fields` cuando el evento **cruza módulos via Kafka** — es necesario para que el id viaje en el payload del Integration Event. El generador lo mapea automáticamente a `event.getAggregateId()` en el handler, evitando la duplicación en el Domain Event class interno.
1620
+
1621
+ **Código generado:**
1622
+
1623
+ ```java
1624
+ public void place() {
1625
+ this.status = this.status.transitionTo(OrderStatus.PLACED);
1626
+ raise(new OrderPlaced(this.getId(), this.getCustomerId(), this.getTotalAmount(), LocalDateTime.now()));
1627
+ // ^—aggregateId ^—customerId ^—totalAmount ^—placedAt
1628
+ }
1629
+
1630
+ public void cancel() {
1631
+ this.status = this.status.transitionTo(OrderStatus.CANCELLED);
1632
+ raise(new OrderCancelled(this.getId(), null /* TODO: provide reason */));
1582
1633
  }
1583
1634
  ```
1584
1635
 
1585
- **Publicar desde la entidad raíz** usando `raise()` heredado:
1636
+ Si un evento **no declara `triggers`**, el desarrollador debe llamar a `raise()` manualmente dentro del método de negocio.
1637
+
1638
+ Genera `OrderPlaced.java` (en `domain/models/events/`) que extiende `DomainEvent`, y `OrderDomainEventHandler.java` (en `application/usecases/`) con `@TransactionalEventListener(AFTER_COMMIT)`.
1639
+
1640
+ **Validaciones del generador:**
1641
+
1642
+ | Código | Severidad | Condición |
1643
+ |---|---|---|
1644
+ | C2-001 | warning | Transición sin use-case — silenciado si tiene `triggers` |
1645
+ | C2-004 | error | `triggers` referencia un método que no existe en ninguna transición |
1646
+ | C2-005 | info | Transición sin ningún evento asociado — considera declarar `triggers` |
1647
+
1648
+ **Auto-wiring de broker:** Si el proyecto tiene `eva add kafka-client` instalado, `eva g entities` genera automáticamente la capa de Integration Events para **todos** los eventos declarados:
1649
+
1650
+ | Archivo generado | Descripción |
1651
+ |---|---|
1652
+ | `application/events/OrderPlacedIntegrationEvent.java` | Record broker-facing (Integration Event) |
1653
+ | `application/ports/MessageBroker.java` | Puerto broker-agnóstico (creado/actualizado) |
1654
+ | `infrastructure/adapters/kafkaMessageBroker/…` | Adaptador Kafka (creado/actualizado) |
1655
+ | `shared/…/kafkaConfig/KafkaConfig.java` | Bean `NewTopic` (actualizado) |
1656
+ | `parameters/*/kafka.yaml` | Configuración de topic (actualizada) |
1657
+
1658
+ **Domain Event vs Integration Event:**
1659
+ - **Domain Event** (`domain/models/events/OrderPlaced.java`) — señal interna del bounded context. Nunca depende de infraestructura.
1660
+ - **Integration Event** (`application/events/OrderPlacedIntegrationEvent.java`) — proyección para el broker. Cambiar de Kafka a RabbitMQ solo requiere cambiar el adaptador `MessageBroker`.
1661
+
1662
+ El `DomainEventHandler` mapea un Domain Event a un Integration Event:
1586
1663
  ```java
1587
- public void confirm() {
1588
- this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
1589
- raise(new OrderConfirmedEvent(this.id, this.id, LocalDateTime.now()));
1664
+ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
1665
+ public void onOrderPlaced(OrderPlaced event) {
1666
+ messageBroker.publishOrderPlacedIntegrationEvent(
1667
+ new OrderPlacedIntegrationEvent(event.getCustomerId(), event.getTotalAmount(), event.getPlacedAt())
1668
+ );
1590
1669
  }
1591
1670
  ```
1592
1671
 
1672
+ **Nota:** el flag `kafka: true` por evento ya **no es necesario** — todos los eventos se cablearán automáticamente cuando haya un broker instalado.
1673
+
1674
+ ### Consumo de Eventos Externos (`listeners[]`)
1675
+
1676
+ ```yaml
1677
+ # Nivel raíz, sibling de aggregates:
1678
+ listeners:
1679
+ - event: PaymentApprovedEvent # PascalCase + sufijo Event
1680
+ producer: payments # Módulo que lo produce (referencia documental)
1681
+ topic: PAYMENT_APPROVED # Topic Kafka — obligatorio en módulos standalone
1682
+ useCase: ConfirmOrder # Caso de uso que maneja el evento (PascalCase)
1683
+ fields: # Campos del payload recibido
1684
+ - name: orderId
1685
+ type: String
1686
+ - name: approvedAt
1687
+ type: LocalDateTime
1688
+ - name: details # Tipo complejo → declarar en nestedTypes
1689
+ type: PaymentDetails
1690
+ nestedTypes: # Records auxiliares para campos de tipo objeto
1691
+ - name: paymentDetails # camelCase → normalizado a PaymentDetails
1692
+ fields:
1693
+ - name: paymentId
1694
+ type: String
1695
+ - name: method
1696
+ type: String
1697
+ - name: amount
1698
+ type: BigDecimal
1699
+ ```
1700
+
1701
+ Genera por cada entrada (hasta **6 artefactos**):
1702
+
1703
+ | Artefacto | Descripción |
1704
+ |---|---|
1705
+ | `application/events/PaymentDetails.java` | Record auxiliar (uno por `nestedTypes` entry) |
1706
+ | `application/events/PaymentApprovedIntegrationEvent.java` | Record tipado con los `fields` declarados |
1707
+ | `infrastructure/kafkaListener/PaymentApprovedKafkaListener.java` | `@KafkaListener` → deserializa y despacha al `useCase` |
1708
+ | `parameters/*/kafka.yaml` | Registro del topic en `topics:` |
1709
+ | `application/commands/ConfirmOrderCommand.java` | Command tipado para el `useCase` |
1710
+ | `application/usecases/ConfirmOrderCommandHandler.java` | Handler stub — implementar la lógica de negocio aquí |
1711
+
1712
+ **Deserialization:** el listener usa `EventEnvelope<Map<String,Object>>` + `objectMapper.convertValue()` para deserializar cada campo del payload de forma robusta y tipada.
1713
+
1714
+ **Regla de `topic:`:**
1715
+ - Módulo standalone (sin `system.yaml`) → `topic:` **obligatorio**
1716
+ - Proyecto con `system.yaml` → puede omitirse; el generador lo infiere
1717
+ - Declarado explícitamente → tiene **precedencia** sobre la inferencia
1718
+
1719
+ **`nestedTypes:` — cuándo usarlo:**
1720
+ Declara un `nestedType` cuando un campo del payload es un **objeto anidado** (no un escalar). El generador produce un record en el mismo paquete `application/events/`, que tanto la `IntegrationEvent` como el `Command` y el `KafkaListener` usarán directamente.
1721
+
1722
+ **Contraste producidos vs. consumidos:**
1723
+ ```
1724
+ aggregates:
1725
+ └── events: → Domain Events que PRODUCE (domain/models/events/)
1726
+
1727
+ listeners: → Integration Events que CONSUME (infrastructure/kafkaListener/)
1728
+ ```
1729
+
1730
+ ---
1731
+
1732
+ ### Clientes HTTP Síncronos (`ports[]`)
1733
+
1734
+ ```yaml
1735
+ # Nivel raíz, sibling de aggregates: y listeners:
1736
+ # Un método = una entrada; entries del mismo service: forman un solo FeignClient.
1737
+
1738
+ ports:
1739
+ - name: findScreeningById # nombre del método (camelCase)
1740
+ service: ScreeningService # agrupa en una interfaz/FeignClient (PascalCase)
1741
+ target: screenings # módulo destino (referencia documental)
1742
+ baseUrl: http://localhost:8081 # → parameters/*/urls.yaml (primera entrada del service)
1743
+ http: GET /screenings/{id} # verbo + path
1744
+ fields: # campos de respuesta → domain model + infra DTO
1745
+ - name: id
1746
+ type: String
1747
+ - name: startTime
1748
+ type: LocalDateTime
1749
+
1750
+ - name: findAvailableSeats
1751
+ service: ScreeningService # mismo service → mismo FeignClient
1752
+ target: screenings
1753
+ http: GET /screenings/{id}/seats
1754
+ returnList: true # → List<Seat> en la interfaz del puerto
1755
+ domainType: Seat # sobrescribe el tipo auto-derivado
1756
+ fields:
1757
+ - name: seatId
1758
+ type: String
1759
+ - name: seatType
1760
+ type: String
1761
+
1762
+ - name: processPayment
1763
+ service: PaymentGateway
1764
+ target: payment-gateway-external
1765
+ baseUrl: https://api.payments.example.com
1766
+ http: POST /payments
1767
+ body: # @RequestBody → ProcessPaymentRequestDto.java
1768
+ - name: amount
1769
+ type: BigDecimal
1770
+ - name: paymentMethod
1771
+ type: PaymentMethodInput # tipo objeto → declarar en nestedTypes:
1772
+ nestedTypes:
1773
+ - name: paymentMethodInput
1774
+ fields:
1775
+ - name: type
1776
+ type: String
1777
+ - name: cardToken
1778
+ type: String
1779
+ fields: # respuesta → domain model Payment + infra DTO
1780
+ - name: paymentId
1781
+ type: String
1782
+ - name: status
1783
+ type: String
1784
+
1785
+ - name: cancelPayment
1786
+ service: PaymentGateway
1787
+ target: payment-gateway-external
1788
+ http: DELETE /payments/{id}
1789
+ # fields: omitido → retorno void
1790
+ ```
1791
+
1792
+ ### Artefactos generados por `ports[]`
1793
+
1794
+ Por cada `service:` único:
1795
+
1796
+ | Archivo generado | Descripción |
1797
+ |---|---|
1798
+ | `domain/repositories/{ServiceName}.java` | Interfaz del puerto secundario (devuelve modelos de dominio) |
1799
+ | `infrastructure/adapters/{service}/{ServiceName}FeignClient.java` | Cliente Feign tipado (devuelve DTOs infra) |
1800
+ | `infrastructure/adapters/{service}/{ServiceName}FeignAdapter.java` | `@Component implements {ServiceName}` — actúa como ACL |
1801
+ | `infrastructure/adapters/{service}/{ServiceName}FeignConfig.java` | Timeouts Feign |
1802
+ | `parameters/*/urls.yaml` | Base URL parametrizada |
1803
+
1804
+ Por cada modelo de dominio único derivado de los métodos con `fields:`:
1805
+
1806
+ | Archivo generado | Descripción |
1807
+ |---|---|
1808
+ | `domain/models/{service}/{DomainType}.java` | Modelo de dominio (ACL) — abstracción interna |
1809
+
1810
+ Por cada método:
1811
+
1812
+ | Archivo generado | Condición |
1813
+ |---|---|
1814
+ | `infrastructure/adapters/{service}/{MethodPascal}Dto.java` | Cuando `fields:` presente — DTO infra (forma externa) |
1815
+ | `application/dtos/{MethodPascal}RequestDto.java` | Cuando `body:` presente (POST/PUT/PATCH) |
1816
+ | `application/dtos/{NestedTypePascal}.java` | Cuando `nestedTypes:` declarado |
1817
+
1818
+ **Patrón ACL:** Los DTOs de infraestructura (forma de la API externa) viven en `infrastructure/adapters/{service}/`. Los modelos de dominio (abstracción interna) viven en `domain/models/{service}/`. El `FeignAdapter` mapea `InfraDto → DomainModel` inline con métodos privados `to{DomainType}()`. Si la API externa cambia, solo hay que actualizar el adaptador.
1819
+
1820
+ ### Reglas de `ports[]`
1821
+
1822
+ - **`service:`** — PascalCase, agrupa métodos en un mismo FeignClient
1823
+ - **`baseUrl:`** — declarar solo en la primera entrada de cada `service:`; si se omite en todas → warning + `http://localhost:8080`
1824
+ - **`body:`** — solo en POST/PUT/PATCH; en GET/DELETE emite warning y se ignora
1825
+ - **`domainType:`** — sobrescribe el tipo de dominio auto-derivado del nombre del método (ej: `domainType: Seat` en `findAvailableSeats`)
1826
+ - **`returnList: true`** — el tipo de retorno es `List<{DomainType}>` en la interfaz y `List<{InfraDto}>` en el FeignClient (default: `false`)
1827
+ - **`nestedTypes:`** — records auxiliares en `application/dtos/`; mismo patrón que `listeners:`
1828
+ - **`fields:` omitido** → retorno `void` en interfaz y FeignClient
1829
+
1830
+ **Contraste async vs sync:**
1831
+ ```
1832
+ aggregates:
1833
+ └── events: → Domain Events que PRODUCE (async, broker)
1834
+ listeners: → Integration Events que CONSUME (async, broker)
1835
+ ports: → Servicios HTTP que LLAMA (sync, Feign)
1836
+ ```
1837
+
1838
+ ---
1839
+
1593
1840
  ### Estrategias de Cascade
1594
1841
 
1595
1842
  ```yaml
@@ -1725,6 +1972,8 @@ public class Product {
1725
1972
 
1726
1973
  ### Reglas para Agentes con Soft Delete
1727
1974
 
1975
+ - **SOLO** aplicar `hasSoftDelete: true` en la **entidad raíz** del agregado (`isRoot: true`)
1976
+ - **NUNCA** poner `hasSoftDelete: true` en entidades secundarias — el ciclo de vida lo controla la raíz mediante `cascade`; el generador emite un warning y descarta el flag
1728
1977
  - **NUNCA** usar `repository.deleteById()` — usar `entity.softDelete()` + `repository.save(entity)`
1729
1978
  - **NUNCA** exponer `deletedAt` en ResponseDtos (campo interno)
1730
1979
  - **SIEMPRE** usar `@SQLRestriction("deleted_at IS NULL")` para filtrado automático en JPA
@@ -1995,8 +2244,10 @@ public record UserResponseDto(
1995
2244
  2. **SI** el módulo requiere ciclo de vida → usar `transitions` + `initialValue` en el enum
1996
2245
  3. **SI** un Value Object tiene comportamiento → declarar `methods` en `domain.yaml`
1997
2246
  4. **SI** ocurren hechos relevantes de negocio → declarar `events[]` en el agregado
1998
- 5. **SI** el evento debe propagarse a otros servicios agregar `kafka: true` al evento
1999
- 6. **DESPUÉS** de generar el `domain.yaml` ejecutar `eva g entities <module>`
2247
+ 5. **SI** el evento debe propagarse vía broker**no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
2248
+ 6. **SI** el módulo consume eventos de otros servicios → declarar `listeners:` (sibling de `aggregates:`) con `topic:` obligatorio en módulos standalone
2249
+ 7. **SI** el módulo llama servicios HTTP externos de forma síncrona → declarar `ports:` (sibling de `aggregates:`) con `baseUrl:` en la primera entrada de cada `service:`
2250
+ 8. **DESPUÉS** de generar el `domain.yaml` → ejecutar `eva g entities <module>`
2000
2251
 
2001
2252
  ### Al Generar Código de Dominio
2002
2253
 
@@ -2089,7 +2340,17 @@ Al generar o modificar código, verificar:
2089
2340
  - [ ] Enum con ciclo de vida → `transitions` + `initialValue`, no setters
2090
2341
  - [ ] Value Object con comportamiento → `methods` en domain.yaml
2091
2342
  - [ ] Evento de dominio → `events[]`, publicar con `raise()` en método de negocio
2092
- - [ ] Evento con Kafka → `kafka: true` en el evento
2343
+ - [ ] Evento con broker**no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
2344
+ - [ ] Distinguir Domain Event (`domain/models/events/`) e Integration Event (`application/events/`) — cambios de broker solo afectan al adaptador `MessageBroker`
2345
+ - [ ] Consumo de eventos externos → declarar en `listeners[]` (nivel raíz); `topic:` obligatorio en módulos standalone
2346
+ - [ ] Cada `listener` genera hasta 6 artefactos: NestedType(s) → IntegrationEvent → KafkaListener → kafka.yaml → Command → CommandHandler
2347
+ - [ ] Campos de tipo objeto en listeners → declarar `nestedTypes:` para generar records auxiliares en `application/events/`
2348
+ - [ ] Clientes HTTP síncronos → declarar en `ports[]` (nivel raíz); `baseUrl:` en la primera entrada de cada `service:`
2349
+ - [ ] Métodos con respuesta → incluir `fields:` en la entrada del puerto; sin `fields:` = retorno `void`
2350
+ - [ ] Respuestas en lista → agregar `returnList: true` en el método correspondiente
2351
+ - [ ] Métodos con cuerpo (POST/PUT/PATCH) → incluir `body:`; campos de tipo objeto en `nestedTypes:`
2352
+ - [ ] Tipo de dominio auto-derivado del nombre del método — usar `domainType:` para sobrescribir si hay colisión
2353
+ - [ ] Cada `service:` en `ports[]` genera: interfaz (devuelve modelos de dominio en `domain/models/{service}/`), FeignClient, FeignAdapter (ACL), FeignConfig + `urls.yaml`
2093
2354
 
2094
2355
  ---
2095
2356