eva4j 1.0.14 → 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 (93) hide show
  1. package/AGENTS.md +268 -6
  2. package/COMMAND_EVALUATION.md +15 -16
  3. package/DOMAIN_YAML_GUIDE.md +430 -14
  4. package/FUTURE_FEATURES.md +1627 -1168
  5. package/README.md +318 -13
  6. package/bin/eva4j.js +18 -14
  7. package/config/defaults.json +1 -0
  8. package/docs/commands/EVALUATE_SYSTEM.md +714 -262
  9. package/docs/commands/GENERATE_ENTITIES.md +599 -6
  10. package/examples/domain-events.yaml +166 -20
  11. package/examples/domain-listeners.yaml +212 -0
  12. package/examples/domain-one-to-many.yaml +1 -0
  13. package/examples/domain-one-to-one.yaml +1 -0
  14. package/examples/domain-ports.yaml +414 -0
  15. package/examples/domain-soft-delete.yaml +47 -44
  16. package/examples/system/notification.yaml +147 -0
  17. package/examples/system/product.yaml +185 -0
  18. package/examples/system/system.yaml +112 -0
  19. package/examples/system-report.html +971 -0
  20. package/examples/system.yaml +46 -3
  21. package/package.json +2 -1
  22. package/src/commands/build.js +714 -0
  23. package/src/commands/create.js +1 -0
  24. package/src/commands/detach.js +1 -0
  25. package/src/commands/evaluate-system.js +234 -8
  26. package/src/commands/generate-entities.js +685 -66
  27. package/src/commands/generate-http-exchange.js +2 -0
  28. package/src/commands/generate-kafka-event.js +43 -10
  29. package/src/generators/base-generator.js +2 -3
  30. package/src/generators/postman-generator.js +188 -0
  31. package/src/generators/shared-generator.js +10 -0
  32. package/src/utils/config-manager.js +54 -0
  33. package/src/utils/context-builder.js +1 -0
  34. package/src/utils/domain-diagram.js +192 -0
  35. package/src/utils/domain-validator.js +970 -0
  36. package/src/utils/fake-data.js +376 -0
  37. package/src/utils/system-validator.js +319 -199
  38. package/src/utils/yaml-to-entity.js +272 -7
  39. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  40. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  41. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +6 -5
  42. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  43. package/templates/aggregate/DomainEventHandler.java.ejs +4 -1
  44. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  45. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  46. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  47. package/templates/base/gradle/build.gradle.ejs +3 -2
  48. package/templates/base/root/AGENTS.md.ejs +306 -45
  49. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +572 -12
  50. package/templates/base/root/skill-build-system-yaml.ejs +1206 -12
  51. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  52. package/templates/crud/Controller.java.ejs +4 -4
  53. package/templates/crud/CreateCommand.java.ejs +4 -0
  54. package/templates/crud/CreateItemDto.java.ejs +4 -0
  55. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  56. package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
  57. package/templates/crud/EndpointsController.java.ejs +6 -6
  58. package/templates/crud/FindByQuery.java.ejs +1 -1
  59. package/templates/crud/FindByQueryHandler.java.ejs +1 -1
  60. package/templates/crud/ListQuery.java.ejs +1 -1
  61. package/templates/crud/ListQueryHandler.java.ejs +8 -8
  62. package/templates/crud/ScaffoldQuery.java.ejs +3 -2
  63. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -2
  64. package/templates/crud/SubEntityAddCommand.java.ejs +4 -0
  65. package/templates/crud/UpdateCommand.java.ejs +4 -0
  66. package/templates/evaluate/report.html.ejs +394 -2
  67. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  68. package/templates/kafka-event/Event.java.ejs +9 -0
  69. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  70. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  71. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  72. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  73. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
  74. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  75. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  76. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  77. package/templates/mock/MockEvent.java.ejs +10 -0
  78. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  79. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  80. package/templates/mock/SpringEventListener.java.ejs +61 -0
  81. package/templates/ports/PortDomainModel.java.ejs +35 -0
  82. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  83. package/templates/ports/PortFeignClient.java.ejs +45 -0
  84. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  85. package/templates/ports/PortInterface.java.ejs +45 -0
  86. package/templates/ports/PortNestedType.java.ejs +28 -0
  87. package/templates/ports/PortRequestDto.java.ejs +30 -0
  88. package/templates/ports/PortResponseDto.java.ejs +28 -0
  89. package/templates/postman/Collection.json.ejs +1 -1
  90. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  91. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
  92. package/src/commands/generate-system.js +0 -243
  93. package/templates/base/root/skill-build-domain-yaml.ejs +0 -292
package/AGENTS.md CHANGED
@@ -464,6 +464,10 @@ aggregates:
464
464
 
465
465
  El `domain.yaml` también soporta una sección `endpoints:` opcional (sibling de `aggregates:`) para declarar los endpoints REST. Ver sección [⚡ Características Avanzadas](#-características-avanzadas-del-domainyaml) para detalles.
466
466
 
467
+ El `domain.yaml` también soporta una sección `listeners:` opcional (sibling de `aggregates:`) para declarar los eventos externos que **consume** este módulo. Ver sección [⚡ Características Avanzadas](#-características-avanzadas-del-domainyaml) para detalles.
468
+
469
+ El `domain.yaml` también soporta una sección `ports:` opcional (sibling de `aggregates:`) para declarar los servicios HTTP síncronos que **llama** este módulo. Ver sección [⚡ Características Avanzadas](#-características-avanzadas-del-domainyaml) para detalles.
470
+
467
471
  ---
468
472
 
469
473
  ## ⚡ Características Avanzadas del domain.yaml
@@ -526,16 +530,98 @@ Genera automáticamente **en el enum**: `VALID_TRANSITIONS`, `canTransitionTo()`
526
530
  aggregates:
527
531
  - name: Order
528
532
  entities: [...]
533
+ enums:
534
+ - name: OrderStatus
535
+ transitions:
536
+ - from: DRAFT
537
+ to: PLACED
538
+ method: place
539
+ - from: PLACED
540
+ to: CANCELLED
541
+ method: cancel
529
542
  events:
530
- - name: OrderConfirmed
543
+ - name: OrderPlaced
544
+ topic: ORDER_PLACED # opcional: sobreescribe el topic auto-derivado
545
+ triggers:
546
+ - place # ← conecta la transición con este evento
531
547
  fields:
532
548
  - name: orderId
533
549
  type: String
534
550
  - name: confirmedAt
535
551
  type: LocalDateTime
552
+
553
+ - name: OrderCancelled
554
+ triggers:
555
+ - cancel
556
+ fields:
557
+ - name: reason # campo no resuelto → null /* TODO: provide reason */
558
+ type: String
559
+ ```
560
+
561
+ #### Propiedad `topic` (opcional)
562
+
563
+ Sobreescribe el nombre del topic Kafka auto-derivado para este evento.
564
+
565
+ **Regla de derivación por defecto:** el generador quita el sufijo `Event` del nombre de la clase antes de convertir a SCREAMING_SNAKE_CASE:
566
+ - `ProductPublishedEvent` → `PRODUCT_PUBLISHED` ✓ (no `PRODUCT_PUBLISHED_EVENT`)
567
+ - `OrderCancelled` → `ORDER_CANCELLED` (sin sufijo, sin cambios)
568
+
569
+ **Cuándo usar `topic:` explícito:**
570
+ - El producer y el consumer de otro módulo deben usar exactamente el mismo nombre.
571
+ - Si el consumer en `listeners[]` declara `topic: MY_CUSTOM_TOPIC`, declara el mismo valor aquí para que el match sea garantizado.
572
+
573
+ ```yaml
574
+ events:
575
+ - name: ProductPublishedEvent
576
+ # topic auto-derivado: PRODUCT_PUBLISHED (sufijo 'Event' eliminado)
577
+ triggers: [publish]
578
+ fields: [...]
579
+
580
+ - name: OrderReadyEvent
581
+ topic: ORDER_READY_FOR_PICKUP # override explícito
582
+ triggers: [markReady]
583
+ fields: [...]
536
584
  ```
537
585
 
538
- Genera `OrderConfirmed.java` (en `domain/models/events/`) que extiende `DomainEvent`, y `OrderDomainEventHandler.java` (en `application/usecases/`) con `@TransactionalEventListener(AFTER_COMMIT)`.
586
+ > **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`.
587
+
588
+ #### Propiedad `triggers`
589
+
590
+ 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.
591
+
592
+ **Reglas de resolución de argumentos (en orden):**
593
+
594
+ | Condición del campo del evento | Argumento generado |
595
+ |---|---|
596
+ | Siempre (primer arg, aggregateId del DomainEvent base) | `this.getId()` |
597
+ | Nombre = `{entityName}Id` (ej: `orderId` en `Order`) | **Ignorado** en el Domain Event class — mapeado a `event.getAggregateId()` en el Integration Event |
598
+ | Nombre coincide con un campo de la entidad | `this.get{Field}()` |
599
+ | Nombre termina en `At` + tipo `LocalDateTime` | `LocalDateTime.now()` |
600
+ | No resuelto | `null /* TODO: provide {fieldName} */` |
601
+
602
+ > **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.
603
+
604
+ **Resultado generado:**
605
+
606
+ ```java
607
+ public void place() {
608
+ this.status = this.status.transitionTo(OrderStatus.PLACED);
609
+ raise(new OrderPlaced(this.getId(), this.getId(), LocalDateTime.now()));
610
+ // ^—aggregateId ^—orderId ^—confirmedAt
611
+ }
612
+
613
+ public void cancel() {
614
+ this.status = this.status.transitionTo(OrderStatus.CANCELLED);
615
+ raise(new OrderCancelled(this.getId(), null /* TODO: provide reason */));
616
+ }
617
+ ```
618
+
619
+ Si un evento **no declara `triggers`**, el desarrollador debe llamar a `raise()` manualmente dentro del método de negocio.
620
+
621
+ **Validaciones generadas:**
622
+ - **C2-004** (error): trigger referencia un método que no existe en ninguna transición del módulo
623
+ - **C2-005** (info): transición sin ningún evento asociado — considera declarar `triggers`
624
+ - **C2-001** se silencia automáticamente para transiciones que ya tienen `triggers`
539
625
 
540
626
  **Auto-wiring de broker:** Si el proyecto tiene un broker de mensajería instalado (`eva add kafka-client`), `eva g entities` genera automáticamente la capa de Integration Events para **todos** los eventos declarados — sin necesidad de ejecutar `eva g kafka-event` por separado:
541
627
 
@@ -572,9 +658,170 @@ public void confirm() {
572
658
 
573
659
  **Nota:** el flag `kafka: true` por evento ya no es necesario — todos los eventos se cablearán automáticamente cuando haya un broker instalado.
574
660
 
661
+ ### Consumo de Eventos Externos (`listeners[]`)
662
+
663
+ ```yaml
664
+ # Nivel raíz, sibling de aggregates:
665
+ listeners:
666
+ - event: PaymentApprovedEvent # PascalCase + sufijo Event
667
+ producer: payments # Módulo que lo produce (referencia documental)
668
+ topic: PAYMENT_APPROVED # Topic Kafka — obligatorio en módulos standalone
669
+ useCase: ConfirmOrder # Caso de uso que maneja el evento (PascalCase)
670
+ fields: # Campos del payload recibido
671
+ - name: orderId
672
+ type: String
673
+ - name: approvedAt
674
+ type: LocalDateTime
675
+ - name: details # Tipo complejo → declarar en nestedTypes
676
+ type: PaymentDetails
677
+ nestedTypes: # Records auxiliares para campos de tipo objeto
678
+ - name: paymentDetails # camelCase → normalizado a PaymentDetails
679
+ fields:
680
+ - name: paymentId
681
+ type: String
682
+ - name: method
683
+ type: String
684
+ - name: amount
685
+ type: BigDecimal
686
+ ```
687
+
688
+ Genera por cada entrada (hasta **6 artefactos**):
689
+
690
+ | Archivo generado | Descripción |
691
+ |---|---|
692
+ | `application/events/PaymentDetails.java` | Record auxiliar (uno por `nestedTypes` entry) |
693
+ | `application/events/PaymentApprovedIntegrationEvent.java` | Record tipado con los `fields` declarados |
694
+ | `infrastructure/kafkaListener/PaymentApprovedKafkaListener.java` | `@KafkaListener` → deserializa y despacha al `useCase` |
695
+ | `parameters/*/kafka.yaml` | Registro del topic en `topics:` |
696
+ | `application/commands/ConfirmOrderCommand.java` | Command tipado para el `useCase` |
697
+ | `application/usecases/ConfirmOrderCommandHandler.java` | Handler stub — implementar la lógica de negocio aquí |
698
+
699
+ **Deserialización:** el listener usa `EventEnvelope<Map<String,Object>>` + `objectMapper.convertValue()` para deserializar cada campo del payload de forma robusta y tipada.
700
+
701
+ **Regla de `topic:`:**
702
+ - Módulo standalone (sin `system.yaml`) → `topic:` **obligatorio**
703
+ - Proyecto con `system.yaml` → puede omitirse; el generador lo infiere de `integrations.async[].topic`
704
+ - Declarado explícitamente → tiene **precedencia** sobre la inferencia
705
+
706
+ **`nestedTypes:` — cuándo usarlo:**
707
+ 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` usan directamente.
708
+
709
+ **Colisión de nombres entre módulos:** cuando varios módulos consumen el mismo evento Kafka, el generador produce clases listener con el mismo nombre (ej: `PaymentApprovedKafkaListener` en `orders` y en `notifications`). Esto es seguro porque el generador usa `@Component("<moduleName>.<listenerClassName>")` para calificar el bean y evitar `ConflictingBeanDefinitionException`. **No se requiere acción del agente** — a diferencia de `ports[]`, donde el nombre de `service:` debe ser único por módulo.
710
+
711
+ **Contraste eventos producidos vs. consumidos:**
712
+ ```
713
+ aggregates:
714
+ └── events: → Domain Events que PRODUCE (domain/models/events/)
715
+
716
+ listeners: → Integration Events que CONSUME (infrastructure/kafkaListener/)
717
+ ```
718
+
575
719
  ---
576
720
 
577
- ## �️ Soft Delete
721
+ ### Clientes HTTP Síncronos (`ports[]`)
722
+
723
+ ```yaml
724
+ # Nivel raíz, sibling de aggregates: y listeners:
725
+ # Un método = una entrada; entries del mismo service: forman un solo FeignClient.
726
+
727
+ ports:
728
+ - name: findScreeningById # nombre del método (camelCase)
729
+ service: ScreeningService # agrupa en una interfaz/FeignClient (PascalCase)
730
+ target: screenings # módulo destino (referencia documental)
731
+ baseUrl: http://localhost:8081 # → parameters/*/urls.yaml (primera entrada del service)
732
+ http: GET /screenings/{id} # verbo + path (igual que en system.yaml exposes:)
733
+ fields: # campos de respuesta → {MethodPascal}ResponseDto.java
734
+ - name: id
735
+ type: String
736
+ - name: startTime
737
+ type: LocalDateTime
738
+
739
+ - name: findAvailableSeats
740
+ service: ScreeningService # mismo service → mismo FeignClient
741
+ target: screenings
742
+ http: GET /screenings/{id}/seats
743
+ returnList: true # → List<FindAvailableSeatResponseDto>
744
+ fields:
745
+ - name: seatId
746
+ type: String
747
+ - name: seatType
748
+ type: String
749
+
750
+ - name: processPayment
751
+ service: PaymentGateway
752
+ target: payment-gateway-external
753
+ baseUrl: https://api.payments.example.com
754
+ http: POST /payments
755
+ body: # @RequestBody → ProcessPaymentRequestDto.java
756
+ - name: amount
757
+ type: BigDecimal
758
+ - name: paymentMethod
759
+ type: PaymentMethodInput # tipo objeto → declarar en nestedTypes:
760
+ nestedTypes:
761
+ - name: paymentMethodInput
762
+ fields:
763
+ - name: type
764
+ type: String
765
+ - name: cardToken
766
+ type: String
767
+ fields: # respuesta → ProcessPaymentResponseDto.java
768
+ - name: paymentId
769
+ type: String
770
+ - name: status
771
+ type: String
772
+
773
+ - name: cancelPayment
774
+ service: PaymentGateway
775
+ target: payment-gateway-external
776
+ http: DELETE /payments/{id}
777
+ # fields: omitido → retorno void
778
+ ```
779
+
780
+ ### Artefactos generados por `ports[]`
781
+
782
+ Por cada `service:` único:
783
+
784
+ | Archivo generado | Descripción |
785
+ |---|---|
786
+ | `domain/repositories/{ServiceName}.java` | Interfaz del puerto secundario (devuelve modelos de dominio) |
787
+ | `infrastructure/adapters/{service}/{ServiceName}FeignClient.java` | Cliente Feign tipado (devuelve DTOs infra) |
788
+ | `infrastructure/adapters/{service}/{ServiceName}FeignAdapter.java` | `@Component implements {ServiceName}` — actúa como ACL |
789
+ | `infrastructure/adapters/{service}/{ServiceName}FeignConfig.java` | Timeouts Feign |
790
+ | `parameters/*/urls.yaml` | Base URL parametrizada |
791
+
792
+ Por cada modelo de dominio único derivado de los métodos con `fields:`:
793
+
794
+ | Archivo generado | Descripción |
795
+ |---|---|
796
+ | `domain/models/{service}/{DomainType}.java` | Modelo de dominio (ACL) — abstracción interna |
797
+
798
+ Por cada método:
799
+
800
+ | Archivo generado | Condición |
801
+ |---|---|
802
+ | `infrastructure/adapters/{service}/{MethodPascal}Dto.java` | Cuando `fields:` presente — DTO infra (forma externa) |
803
+ | `application/dtos/{MethodPascal}RequestDto.java` | Cuando `body:` presente (POST/PUT/PATCH) |
804
+ | `application/dtos/{NestedTypePascal}.java` | Cuando `nestedTypes:` declarado |
805
+
806
+ **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.
807
+
808
+ ### Reglas de `ports[]`
809
+
810
+ - **`service:`** — PascalCase, agrupa métodos en un mismo FeignClient. **Si varios módulos llaman al mismo servicio externo, cada módulo debe usar un nombre de `service:` propio que refleje su bounded context** (ej: `OrderCustomerService` en `orders`, `DeliveryCustomerService` en `deliveries`). Reutilizar el mismo nombre (`CustomerService`) en módulos distintos causa colisión de beans Spring (`ConflictingBeanDefinitionException`) porque el generador produce un `FeignAdapter` con el mismo nombre de clase en cada módulo
811
+ - **`baseUrl:`** — declarar solo en la primera entrada de cada `service:`; si se omite en todas → warning + `http://localhost:8080`
812
+ - **`body:`** — solo en POST/PUT/PATCH; en GET/DELETE emite warning y se ignora
813
+ - **`domainType:`** — sobrescribe el tipo de dominio auto-derivado del nombre del método (ej: `domainType: Seat` en `findAvailableSeats`)
814
+ - **`returnList: true`** — el tipo de retorno es `List<{DomainType}>` en la interfaz y `List<{InfraDto}>` en el FeignClient (default: `false`)
815
+ - **`nestedTypes:`** — records auxiliares en `application/dtos/`; mismo patrón que `listeners:`
816
+ - **`fields:` omitido** → retorno `void` en interfaz y FeignClient
817
+
818
+ **Contraste async vs sync:**
819
+ ```
820
+ aggregates:
821
+ └── events: → Domain Events que PRODUCE (async, broker)
822
+ listeners: → Integration Events que CONSUME (async, broker)
823
+ ports: → Servicios HTTP que LLAMA (sync, Feign)
824
+ ```
578
825
 
579
826
  Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera eliminación lógica en lugar de física.
580
827
 
@@ -627,6 +874,8 @@ public class Product {
627
874
 
628
875
  ### Reglas para Agentes
629
876
 
877
+ - **SOLO** aplicar `hasSoftDelete: true` en la **entidad raíz** del agregado (`isRoot: true`)
878
+ - **NUNCA** poner `hasSoftDelete: true` en entidades secundarias — el ciclo de vida de estas lo controla la raíz mediante `cascade`; si se ignora, el generador emite un warning y descarta el flag
630
879
  - **NUNCA** usar `repository.deleteById()` cuando hay soft delete
631
880
  - **SIEMPRE** usar `entity.softDelete()` + `repository.save(entity)`
632
881
  - **NUNCA** exponer `deletedAt` en ResponseDtos
@@ -993,7 +1242,7 @@ private String customerId;
993
1242
  1. **SIEMPRE** declarar `endpoints:` cuando el API REST tiene comportamientos custom (confirmar, cancelar, activar, etc.)
994
1243
  2. **NUNCA** usar `endpoints:` si solo necesitas CRUD estándar — el flujo interactivo es más simple
995
1244
  3. **SIEMPRE** usar PascalCase para los nombres de `useCase` (ej: `ConfirmOrder`, no `confirmOrder`)
996
- 4. **CONOCER** cuáles son los 5 use cases estándar por aggregate: `Create{E}`, `Update{E}`, `Delete{E}`, `Get{E}`, `FindAll{E}s` — estos generan implementación completa
1245
+ 4. **CONOCER** cuáles son los 5 use cases estándar por aggregate: `Create{E}`, `Update{E}`, `Delete{E}`, `Get{E}`, `FindAll{Plural(E)}` — estos generan implementación completa (e.g. `FindAllOrders`, `FindAllDeliveries`, `FindAllCategories`)
997
1246
  5. **SABER** que cualquier otro nombre genera un **scaffold** con `UnsupportedOperationException` — el desarrollador debe implementar el handler
998
1247
  6. **APLICAR** la regla anti-duplicado: si el mismo useCase aparece en v1 y v2, se genera solo una vez
999
1248
  7. **NOMBRAR** los controladores según la convención: `{Aggregate}{VersionCapitalized}Controller` (ej: `OrderV1Controller`)
@@ -1228,12 +1477,25 @@ Al generar o modificar código, verificar:
1228
1477
  - [ ] Enum con ciclo de vida → usar `transitions` + `initialValue`, no setters manuales
1229
1478
  - [ ] Value Object con comportamiento → declarar `methods` en lugar de lógica en entidad
1230
1479
  - [ ] Evento de dominio → declarar en `events[]`, publicar con `raise()` en método de negocio
1480
+ - [ ] Evento con `triggers: [methodName]` → el generador emite `raise()` automáticamente; args no resolubles quedan como `null /* TODO */`
1481
+ - [ ] Sin `triggers` en el evento → el dev llama a `raise()` manualmente
1231
1482
  - [ ] Evento con broker → **no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
1232
1483
  - [ ] Distinguir entre Domain Event (`domain/models/events/X.java`) e Integration Event (`application/events/XIntegrationEvent.java`) — cambios de broker solo afectan al adaptador `MessageBroker`
1484
+ - [ ] Consumo de eventos externos → declarar en `listeners[]` (nivel raíz); `topic:` obligatorio en módulos standalone
1485
+ - [ ] Cada `listener` genera hasta 6 artefactos: NestedType(s) → IntegrationEvent → KafkaListener → kafka.yaml → Command → CommandHandler
1486
+ - [ ] Varios módulos pueden consumir el mismo evento Kafka sin colisión — el generador califica el bean automáticamente con `@Component("moduleName.listenerClassName")`
1487
+ - [ ] Campos de tipo objeto en listeners → declarar `nestedTypes:` para generar records auxiliares en `application/events/`
1233
1488
  - [ ] Endpoints REST específicos → declarar `endpoints:` con versiones y operaciones; usar nombres estándar para implementación completa
1489
+ - [ ] Clientes HTTP síncronos → declarar en `ports[]` (nivel raíz); `baseUrl:` en la primera entrada de cada `service:`
1490
+ - [ ] Si varios módulos llaman al mismo servicio → cada uno usa un `service:` con nombre propio del bounded context (ej: `OrderCustomerService`, `DeliveryCustomerService`) — nunca el mismo nombre genérico en módulos distintos
1491
+ - [ ] Métodos con respuesta → incluir `fields:` en la entrada del puerto; sin `fields:` = retorno `void`
1492
+ - [ ] Respuestas en lista → agregar `returnList: true` en el método correspondiente
1493
+ - [ ] Métodos con cuerpo (POST/PUT/PATCH) → incluir `body:`; campos de tipo objeto en `nestedTypes:`
1494
+ - [ ] Tipo de dominio auto-derivado del nombre del método — usar `domainType:` para sobrescribir si es necesario
1495
+ - [ ] Cada `service:` en `ports[]` genera: interfaz (devuelve modelos de dominio), FeignClient (devuelve DTOs infra), FeignAdapter (mapea ACL), FeignConfig + `urls.yaml`
1234
1496
 
1235
1497
  ---
1236
1498
 
1237
- **Última actualización:** 2026-03-11
1238
- **Versión de eva4j:** 1.0.13
1499
+ **Última actualización:** 2026-03-12
1500
+ **Versión de eva4j:** 1.0.14
1239
1501
  **Estado:** Documento de referencia para agentes IA
@@ -412,20 +412,20 @@ aggregates:
412
412
 
413
413
  ---
414
414
 
415
- ### 4. Soft Delete (0% implementado)
415
+ ### 4. Soft Delete Implementado
416
416
 
417
417
  **Impacto**: Medio - Común en apps business
418
418
 
419
- #### Casos de Uso No Cubiertos
419
+ #### Sintaxis
420
420
  ```yaml
421
- # No soportado actualmente
422
- rootEntity:
423
- name: order
424
- softDelete: true # Debería agregar deletedAt y lógica
421
+ # Soportado solo en la entidad raíz (isRoot: true)
422
+ entities:
423
+ - name: order
424
+ isRoot: true
425
+ hasSoftDelete: true # Genera deletedAt, softDelete(), @SQLRestriction
425
426
  ```
426
427
 
427
- **Esfuerzo para implementar**: Medio (4-5 horas)
428
- **Prioridad**: 🟡 Media
428
+ **Estado**: Implementado en `yaml-to-entity.js` + templates `AggregateRoot`, `JpaAggregateRoot`, repositorios y `DeleteCommandHandler`.
429
429
 
430
430
  ---
431
431
 
@@ -558,7 +558,7 @@ aggregates:
558
558
 
559
559
  1. ❌ **Validaciones JSR-303** (0%)
560
560
  2. ❌ **Auditoría (createdAt, updatedAt)** (0%)
561
- 3. **Soft delete** (0%)
561
+ 3. **Soft delete** (implementado con `hasSoftDelete: true`)
562
562
  4. ❌ **Índices y constraints** (0%)
563
563
  5. ❌ **Herencia de entidades** (0%)
564
564
  6. ❌ **DTOs de aplicación** (0%)
@@ -598,7 +598,7 @@ aggregates:
598
598
 
599
599
  | # | Mejora | Impacto | Esfuerzo | Prioridad |
600
600
  |---|--------|---------|----------|-----------|
601
- | 5 | Soft delete | 🟡 Medio | 5h | 🟡 Media |
601
+ | 5 | ~~Soft delete~~ | Implementado | | |
602
602
  | 6 | ManyToMany completo | 🟡 Medio | 6h | 🟡 Media |
603
603
  | 7 | OneToOne avanzado | 🟡 Bajo | 4h | 🔵 Baja |
604
604
 
@@ -640,9 +640,7 @@ aggregates:
640
640
 
641
641
  ### Mediano Plazo
642
642
 
643
- 4. **Soft Delete**
644
- - Útil para muchas apps
645
- - Buena relación esfuerzo/beneficio
643
+ 4. ~~**Soft Delete**~~ ✅ Implementado con `hasSoftDelete: true`
646
644
 
647
645
  5. **ManyToMany completo**
648
646
  - Completa el soporte de relaciones JPA
@@ -794,15 +792,16 @@ entities:
794
792
 
795
793
  ---
796
794
 
797
- ### 4. Soft Delete
795
+ ### 4. Soft Delete ✅ Implementado
798
796
 
799
797
  ```yaml
800
798
  entities:
801
799
  - name: Order
802
- softDelete: true # Agrega deletedAt y lógica
800
+ isRoot: true
801
+ hasSoftDelete: true # Genera deletedAt, softDelete(), isDeleted(), @SQLRestriction
803
802
  ```
804
803
 
805
- **Implementación**: Campo `deletedAt` + custom queries en repositorio.
804
+ **Implementado**: `deletedAt` inyectado automáticamente, `@SQLRestriction("deleted_at IS NULL")` en JPA, `softDelete()` + `isDeleted()` en dominio, `DeleteCommandHandler` usa borrado lógico.
806
805
 
807
806
  ---
808
807