eva4j 1.0.12 → 1.0.14

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 (66) hide show
  1. package/AGENTS.md +468 -9
  2. package/DOMAIN_YAML_GUIDE.md +524 -21
  3. package/FUTURE_FEATURES.md +94 -1
  4. package/bin/eva4j.js +31 -1
  5. package/design-system.md +797 -0
  6. package/docs/commands/EVALUATE_SYSTEM.md +542 -0
  7. package/docs/commands/GENERATE_ENTITIES.md +399 -2355
  8. package/docs/commands/GENERATE_RECORD.md +335 -311
  9. package/docs/commands/INDEX.md +10 -1
  10. package/examples/doctor-evaluation.yaml +3 -3
  11. package/examples/domain-audit-complete.yaml +2 -2
  12. package/examples/domain-collections.yaml +2 -2
  13. package/examples/domain-ecommerce.yaml +2 -2
  14. package/examples/domain-endpoints-relations.yaml +353 -0
  15. package/examples/domain-endpoints-versioned.yaml +144 -0
  16. package/examples/domain-endpoints.yaml +135 -0
  17. package/examples/domain-events.yaml +1 -1
  18. package/examples/domain-field-visibility.yaml +11 -5
  19. package/examples/domain-multi-aggregate.yaml +6 -6
  20. package/examples/domain-one-to-many.yaml +1 -1
  21. package/examples/domain-one-to-one.yaml +1 -1
  22. package/examples/domain-secondary-onetomany.yaml +1 -1
  23. package/examples/domain-secondary-onetoone.yaml +1 -1
  24. package/examples/domain-simple.yaml +1 -1
  25. package/examples/domain-soft-delete.yaml +3 -3
  26. package/examples/domain-transitions.yaml +1 -1
  27. package/examples/domain-value-objects.yaml +1 -1
  28. package/examples/system.yaml +289 -0
  29. package/package.json +1 -1
  30. package/src/commands/create.js +6 -3
  31. package/src/commands/evaluate-system.js +384 -0
  32. package/src/commands/generate-entities.js +677 -14
  33. package/src/commands/generate-kafka-event.js +59 -5
  34. package/src/commands/generate-system.js +243 -0
  35. package/src/generators/base-generator.js +9 -1
  36. package/src/utils/naming.js +3 -2
  37. package/src/utils/system-validator.js +314 -0
  38. package/src/utils/yaml-to-entity.js +100 -4
  39. package/templates/aggregate/AggregateRepository.java.ejs +5 -0
  40. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +9 -0
  41. package/templates/aggregate/AggregateRoot.java.ejs +5 -1
  42. package/templates/aggregate/DomainEntity.java.ejs +5 -1
  43. package/templates/aggregate/DomainEventHandler.java.ejs +24 -20
  44. package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -1
  45. package/templates/aggregate/JpaEntity.java.ejs +2 -1
  46. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  47. package/templates/base/root/AGENTS.md.ejs +916 -51
  48. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1103 -0
  49. package/templates/base/root/skill-build-domain-yaml.ejs +292 -0
  50. package/templates/base/root/skill-build-system-yaml.ejs +252 -0
  51. package/templates/base/root/system.yaml.ejs +97 -0
  52. package/templates/crud/EndpointsController.java.ejs +178 -0
  53. package/templates/crud/FindByQuery.java.ejs +17 -0
  54. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  55. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  56. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  57. package/templates/crud/ScaffoldQuery.java.ejs +12 -0
  58. package/templates/crud/ScaffoldQueryHandler.java.ejs +40 -0
  59. package/templates/crud/SubEntityAddCommand.java.ejs +17 -0
  60. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  61. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  62. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  63. package/templates/crud/TransitionCommand.java.ejs +9 -0
  64. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  65. package/templates/evaluate/report.html.ejs +971 -0
  66. package/templates/kafka-event/Event.java.ejs +7 -0
package/AGENTS.md CHANGED
@@ -451,11 +451,223 @@ aggregates:
451
451
  - ACTIVE
452
452
  - INACTIVE
453
453
  - SUSPENDED
454
+
455
+ events:
456
+ - name: UserRegisteredEvent
457
+ fields:
458
+ - name: userId
459
+ type: String
460
+ # Nota: el flag kafka: true ya no es necesario.
461
+ # Si el proyecto tiene un broker instalado (eva add kafka-client),
462
+ # eva g entities cablea automáticamente todos los eventos declarados.
463
+ ```
464
+
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
+
467
+ ---
468
+
469
+ ## ⚡ Características Avanzadas del domain.yaml
470
+
471
+ ### Value Objects con Métodos
472
+
473
+ Los Value Objects pueden declarar métodos de negocio directamente en `domain.yaml`:
474
+
475
+ ```yaml
476
+ valueObjects:
477
+ - name: Money
478
+ fields:
479
+ - name: amount
480
+ type: BigDecimal
481
+ - name: currency
482
+ type: String
483
+ methods:
484
+ - name: add
485
+ returnType: Money
486
+ parameters:
487
+ - name: other
488
+ type: Money
489
+ body: "return new Money(this.amount.add(other.getAmount()), this.currency);"
490
+ - name: isPositive
491
+ returnType: boolean
492
+ parameters: []
493
+ body: "return this.amount.compareTo(BigDecimal.ZERO) > 0;"
494
+ ```
495
+
496
+ ### Enums con Ciclo de Vida (Transitions)
497
+
498
+ Cuando un enum representa estados de negocio, declara `transitions` e `initialValue`:
499
+
500
+ ```yaml
501
+ enums:
502
+ - name: OrderStatus
503
+ initialValue: PENDING # Auto-inicializa en constructor; excluido del CreateDto
504
+ transitions:
505
+ - from: PENDING
506
+ to: CONFIRMED
507
+ method: confirm
508
+ - from: CONFIRMED
509
+ to: SHIPPED
510
+ method: ship
511
+ - from: [PENDING, CONFIRMED] # múltiples orígenes
512
+ to: CANCELLED
513
+ method: cancel
514
+ guard: "this.status == OrderStatus.DELIVERED" # lanza BusinessException si se cumple
515
+ values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
516
+ ```
517
+
518
+ Genera automáticamente **en la entidad raíz**: `confirm()`, `ship()`, `cancel()`, helpers `isPending()`, `isConfirmed()`, `canConfirm()`, `canCancel()`.
519
+ Genera automáticamente **en el enum**: `VALID_TRANSITIONS`, `canTransitionTo()`, `transitionTo()` (lanza `InvalidStateTransitionException` si la transición no es válida).
520
+
521
+ **Nota:** El campo con `initialValue` se trata como `readOnly: true` — no aparece en el constructor de negocio ni en el `CreateDto`.
522
+
523
+ ### Eventos de Dominio (`events[]`)
524
+
525
+ ```yaml
526
+ aggregates:
527
+ - name: Order
528
+ entities: [...]
529
+ events:
530
+ - name: OrderConfirmed
531
+ fields:
532
+ - name: orderId
533
+ type: String
534
+ - name: confirmedAt
535
+ type: LocalDateTime
536
+ ```
537
+
538
+ Genera `OrderConfirmed.java` (en `domain/models/events/`) que extiende `DomainEvent`, y `OrderDomainEventHandler.java` (en `application/usecases/`) con `@TransactionalEventListener(AFTER_COMMIT)`.
539
+
540
+ **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
+
542
+ | Archivo generado | Descripción |
543
+ |---|---|
544
+ | `application/events/OrderConfirmedIntegrationEvent.java` | Record broker-facing (Integration Event) |
545
+ | `application/ports/MessageBroker.java` | Puerto broker-agnóstico (creado/actualizado) |
546
+ | `infrastructure/adapters/kafkaMessageBroker/…` | Adaptador Kafka (creado/actualizado) |
547
+ | `shared/…/kafkaConfig/KafkaConfig.java` | Bean `NewTopic` (actualizado) |
548
+ | `parameters/*/kafka.yaml` | Configuración de topic (actualizada) |
549
+
550
+ **Domain Event vs Integration Event:**
551
+ - **Domain Event** (`domain/models/events/OrderConfirmed.java`) — señal interna del bounded context. Nunca depende de infraestructura.
552
+ - **Integration Event** (`application/events/OrderConfirmedIntegrationEvent.java`) — proyección para el broker. Cambiar de Kafka a RabbitMQ solo requiere cambiar el adaptador `MessageBroker`; los Domain Events no se modifican nunca.
553
+
554
+ El `DomainEventHandler` mapea un Domain Event a un Integration Event:
555
+ ```java
556
+ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
557
+ public void onOrderConfirmed(OrderConfirmed event) {
558
+ messageBroker.publishOrderConfirmedIntegrationEvent(
559
+ new OrderConfirmedIntegrationEvent(event.getOrderId(), event.getConfirmedAt())
560
+ );
561
+ }
562
+ ```
563
+
564
+ Publicar desde la entidad raíz usando `raise()` heredado:
565
+
566
+ ```java
567
+ public void confirm() {
568
+ this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
569
+ raise(new OrderConfirmed(this.id, LocalDateTime.now()));
570
+ }
571
+ ```
572
+
573
+ **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
+
575
+ ---
576
+
577
+ ## �️ Soft Delete
578
+
579
+ Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera eliminación lógica en lugar de física.
580
+
581
+ ### Configuración en domain.yaml
582
+
583
+ ```yaml
584
+ entities:
585
+ - name: product
586
+ isRoot: true
587
+ tableName: products
588
+ hasSoftDelete: true # ✅ Activa soft delete
589
+ audit:
590
+ enabled: true
591
+ fields:
592
+ - name: id
593
+ type: String
594
+ - name: name
595
+ type: String
596
+ ```
597
+
598
+ ### Comportamiento generado
599
+
600
+ ```java
601
+ // Entidad JPA — filtrado automático con @SQLRestriction
602
+ @Entity
603
+ @SQLRestriction("deleted_at IS NULL")
604
+ public class ProductJpa extends AuditableEntity {
605
+ @Column(name = "deleted_at")
606
+ private LocalDateTime deletedAt;
607
+ }
454
608
  ```
455
609
 
610
+ ```java
611
+ // Entidad de dominio — método de negocio
612
+ public class Product {
613
+ private LocalDateTime deletedAt;
614
+
615
+ public void softDelete() {
616
+ if (this.deletedAt != null) {
617
+ throw new IllegalStateException("Product is already deleted");
618
+ }
619
+ this.deletedAt = LocalDateTime.now();
620
+ }
621
+
622
+ public boolean isDeleted() {
623
+ return this.deletedAt != null;
624
+ }
625
+ }
626
+ ```
627
+
628
+ ### Reglas para Agentes
629
+
630
+ - **NUNCA** usar `repository.deleteById()` cuando hay soft delete
631
+ - **SIEMPRE** usar `entity.softDelete()` + `repository.save(entity)`
632
+ - **NUNCA** exponer `deletedAt` en ResponseDtos
633
+ - **SIEMPRE** usar `@SQLRestriction("deleted_at IS NULL")` en la entidad JPA
634
+
456
635
  ---
457
636
 
458
- ## 🚨 Errores Comunes a Evitar
637
+ ## ⏱️ Temporal Workflows
638
+
639
+ Cuando se agrega soporte de Temporal con `eva add temporal-client`, se genera infraestructura para workflows duraderos.
640
+
641
+ ### Archivos generados por `eva g temporal-flow <module>`
642
+
643
+ ```
644
+ [module]/
645
+ ├── application/workflows/
646
+ │ ├── OrderWorkflow.java # Interface (@WorkflowInterface)
647
+ │ └── OrderWorkflowImpl.java # Implementación (determinista)
648
+ └── infrastructure/temporal/
649
+ ├── activities/
650
+ │ ├── OrderActivity.java # Interface (@ActivityInterface)
651
+ │ └── OrderActivityImpl.java # Implementación (con I/O)
652
+ └── workers/
653
+ └── OrderWorker.java # Registro del worker
654
+ ```
655
+
656
+ ### Principios clave
657
+
658
+ - Los **Workflows deben ser deterministas** — sin `Math.random()`, `new Date()`, ni I/O
659
+ - Toda operación con efectos secundarios (DB, HTTP, emails) va en **Activities**
660
+ - Los **Use Cases** orquestan los workflows; las **Activities** ejecutan infraestructura
661
+ - Configuración de conexión en `resources/parameters/{env}/temporal.yaml`
662
+
663
+ ### Templates relacionados en eva4j
664
+
665
+ - `templates/temporal-flow/` — workflow interface e implementación
666
+ - `templates/temporal-activity/` — activity interface, implementación y worker
667
+
668
+ ---
669
+
670
+ ## �🚨 Errores Comunes a Evitar
459
671
 
460
672
  ### ❌ NO Crear Constructor Vacío en Dominio
461
673
 
@@ -566,6 +778,7 @@ Los campos en domain.yaml soportan las siguientes propiedades:
566
778
  | `enumValues` | Array | `[]` | Valores inline de enum |
567
779
  | **`readOnly`** | Boolean | `false` | **Excluye del constructor de negocio y CreateDto** |
568
780
  | **`hidden`** | Boolean | `false` | **Excluye del ResponseDto** |
781
+ | **`defaultValue`** | String/Number/Boolean | `null` | **Valor inicial en el constructor de creación (solo para `readOnly`)** |
569
782
  | **`validations`** | Array | `[]` | **Anotaciones JSR-303 en el Command y CreateDto** |
570
783
  | **`reference`** | Object | `null` | **Declara referencia semántica a otro agregado (genera comentario Javadoc)** |
571
784
 
@@ -601,9 +814,57 @@ fields:
601
814
  |-------|---------------------|-----------|-------------|
602
815
  | Normal | ✅ | ✅ | ✅ |
603
816
  | `readOnly: true` | ❌ | ❌ | ✅ |
817
+ | `readOnly` + `defaultValue` | ⚡ Asignado con default | ❌ | ✅ |
604
818
  | `hidden: true` | ✅ | ✅ | ❌ |
605
819
  | Ambos flags | ❌ | ❌ | ❌ |
606
820
 
821
+ #### 🎯 `defaultValue` - Valor Inicial para campos `readOnly`
822
+
823
+ Cuando un campo `readOnly` tiene un valor inicial predecible (ej: contadores, estados iniciales), se puede declarar en `domain.yaml` con `defaultValue`. El generador emite la asignación en el **constructor de creación** del dominio y `@Builder.Default` en la entidad JPA.
824
+
825
+ ```yaml
826
+ fields:
827
+ - name: totalAmount
828
+ type: BigDecimal
829
+ readOnly: true
830
+ defaultValue: "0.00" # ✅ Acumulador inicializado
831
+
832
+ - name: status
833
+ type: OrderStatus
834
+ readOnly: true
835
+ defaultValue: PENDING # ✅ Estado inicial del enum
836
+
837
+ - name: itemCount
838
+ type: Integer
839
+ readOnly: true
840
+ defaultValue: 0 # ✅ Contador inicializado
841
+ ```
842
+
843
+ ```java
844
+ // Constructor de creación — defaultValues aplicados
845
+ public Order(String orderNumber, String customerId) {
846
+ this.orderNumber = orderNumber;
847
+ this.customerId = customerId;
848
+ this.totalAmount = new BigDecimal("0.00"); // ← defaultValue
849
+ this.status = OrderStatus.PENDING; // ← defaultValue
850
+ this.itemCount = 0; // ← defaultValue
851
+ }
852
+ ```
853
+
854
+ ```java
855
+ // JPA — @Builder.Default para respetar el valor en el builder
856
+ @Builder.Default
857
+ private BigDecimal totalAmount = new BigDecimal("0.00");
858
+
859
+ @Enumerated(EnumType.STRING)
860
+ @Builder.Default
861
+ private OrderStatus status = OrderStatus.PENDING;
862
+ ```
863
+
864
+ **Restricción:** `defaultValue` **solo aplica** a campos con `readOnly: true`. Usarlo en un campo no-readOnly genera un warning y es ignorado.
865
+
866
+ ---
867
+
607
868
  **Ejemplo práctico:**
608
869
  ```yaml
609
870
  entities:
@@ -626,6 +887,87 @@ entities:
626
887
  hidden: true
627
888
  ```
628
889
 
890
+ ### Validaciones JSR-303 (`validations`)
891
+
892
+ Se declaran en el campo y se aplican **únicamente** en el `Command` y `CreateDto` de la capa de aplicación. **Nunca** en las entidades de dominio.
893
+
894
+ ```yaml
895
+ fields:
896
+ - name: email
897
+ type: String
898
+ validations:
899
+ - type: NotBlank
900
+ message: "Email es requerido"
901
+ - type: Email
902
+ message: "Email inválido"
903
+
904
+ - name: username
905
+ type: String
906
+ validations:
907
+ - type: Size
908
+ min: 3
909
+ max: 50
910
+ message: "Username entre 3 y 50 caracteres"
911
+
912
+ - name: age
913
+ type: Integer
914
+ validations:
915
+ - type: Min
916
+ value: 18
917
+ - type: Max
918
+ value: 120
919
+
920
+ - name: price
921
+ type: BigDecimal
922
+ validations:
923
+ - type: Positive
924
+ ```
925
+
926
+ **Anotaciones disponibles:** `NotNull`, `NotBlank`, `NotEmpty`, `Email`, `Size` (min/max), `Min` (value), `Max` (value), `Pattern` (regexp), `Digits` (integer/fraction), `Positive`, `PositiveOrZero`, `Negative`, `Past`, `Future`, `AssertTrue`, `AssertFalse`.
927
+
928
+ **Código generado en `CreateUserCommand.java`:**
929
+ ```java
930
+ public record CreateUserCommand(
931
+ @NotBlank(message = "Email es requerido")
932
+ @Email(message = "Email inválido")
933
+ String email,
934
+
935
+ @Size(min = 3, max = 50, message = "Username entre 3 y 50 caracteres")
936
+ String username
937
+ ) implements Command {}
938
+ ```
939
+
940
+ ### Referencias entre Agregados (`reference`)
941
+
942
+ Declara explícitamente que un campo es un ID de otro agregado. El tipo Java **no cambia** — sigue siendo `String`, `Long`, etc. — pero se genera un comentario Javadoc que documenta la dependencia. **No genera `@ManyToOne`** (correcto en DDD: cada agregado es una unidad transaccional independiente).
943
+
944
+ ```yaml
945
+ fields:
946
+ - name: customerId
947
+ type: String
948
+ reference:
949
+ aggregate: Customer # Nombre del agregado referenciado (PascalCase)
950
+ module: customers # Módulo donde vive (opcional si es el mismo módulo)
951
+
952
+ - name: productId
953
+ type: String
954
+ reference:
955
+ aggregate: Product
956
+ module: catalog
957
+ ```
958
+
959
+ **Código generado:**
960
+ ```java
961
+ // En Order.java (domain entity)
962
+ /** Cross-aggregate reference → Customer (module: customers) */
963
+ private String customerId;
964
+
965
+ // En OrderJpa.java
966
+ @Column(name = "customer_id")
967
+ /** Cross-aggregate reference → Customer (module: customers) */
968
+ private String customerId;
969
+ ```
970
+
629
971
  ### Tipos de Relaciones
630
972
 
631
973
  - `OneToOne` - Relación uno a uno
@@ -637,6 +979,25 @@ entities:
637
979
 
638
980
  ## 🎯 Mejores Prácticas para Agentes
639
981
 
982
+ ### Al Generar domain.yaml (Flujo SDD)
983
+
984
+ 1. **SIEMPRE** incluir campo `id` en todas las entidades
985
+ 2. **SI** el módulo requiere ciclo de vida → usar `transitions` + `initialValue` en el enum
986
+ 3. **SI** un valor tiene lógica de negocio → declararlo como `valueObject` con `methods`
987
+ 4. **SI** ocurren hechos relevantes de negocio → declarar `events[]` en el agregado
988
+ 5. **SI** el módulo expone endpoints REST específicos → declarar `endpoints:` con versiones y operaciones
989
+ 6. **DESPUÉS** de generar el `domain.yaml` → ejecutar `eva g entities <module>`
990
+
991
+ ### Al Usar `endpoints:` en domain.yaml
992
+
993
+ 1. **SIEMPRE** declarar `endpoints:` cuando el API REST tiene comportamientos custom (confirmar, cancelar, activar, etc.)
994
+ 2. **NUNCA** usar `endpoints:` si solo necesitas CRUD estándar — el flujo interactivo es más simple
995
+ 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
997
+ 5. **SABER** que cualquier otro nombre genera un **scaffold** con `UnsupportedOperationException` — el desarrollador debe implementar el handler
998
+ 6. **APLICAR** la regla anti-duplicado: si el mismo useCase aparece en v1 y v2, se genera solo una vez
999
+ 7. **NOMBRAR** los controladores según la convención: `{Aggregate}{VersionCapitalized}Controller` (ej: `OrderV1Controller`)
1000
+
640
1001
  ### Al Generar Código de Dominio
641
1002
 
642
1003
  1. **NUNCA** crear constructor vacío en entidades de dominio
@@ -722,13 +1083,13 @@ HTTP Response (sin createdBy/updatedBy)
722
1083
 
723
1084
  ## 🧪 Testing
724
1085
 
725
- ### Tests de Dominio
1086
+ ### Tests de Dominio (Unidad Pura)
726
1087
 
727
1088
  ```java
728
1089
  @Test
729
1090
  void shouldCreateUserWithValidData() {
730
1091
  User user = new User("john", "john@example.com");
731
-
1092
+
732
1093
  assertEquals("john", user.getUsername());
733
1094
  assertEquals("john@example.com", user.getEmail());
734
1095
  }
@@ -736,13 +1097,82 @@ void shouldCreateUserWithValidData() {
736
1097
  @Test
737
1098
  void shouldValidateBusinessRules() {
738
1099
  User user = new User("john", "john@example.com");
739
-
1100
+
740
1101
  assertThrows(IllegalArgumentException.class, () -> {
741
1102
  user.changeEmail("invalid-email");
742
1103
  });
743
1104
  }
744
1105
  ```
745
1106
 
1107
+ ### Object Mother Pattern
1108
+
1109
+ ```java
1110
+ // src/test/java/[package]/user/domain/UserMother.java
1111
+ public class UserMother {
1112
+
1113
+ public static User valid() {
1114
+ return new User("john_doe", "john@example.com");
1115
+ }
1116
+
1117
+ public static User withEmail(String email) {
1118
+ return new User("john_doe", email);
1119
+ }
1120
+ }
1121
+ ```
1122
+
1123
+ ### Repositorio Fake (In-Memory)
1124
+
1125
+ Para testear Use Cases sin base de datos:
1126
+
1127
+ ```java
1128
+ public class UserRepositoryFake implements UserRepository {
1129
+ private final Map<String, User> store = new HashMap<>();
1130
+
1131
+ @Override
1132
+ public User save(User user) {
1133
+ store.put(user.getId(), user);
1134
+ return user;
1135
+ }
1136
+
1137
+ @Override
1138
+ public Optional<User> findById(String id) {
1139
+ return Optional.ofNullable(store.get(id));
1140
+ }
1141
+
1142
+ public int count() { return store.size(); }
1143
+ }
1144
+ ```
1145
+
1146
+ ### Tests de Use Cases
1147
+
1148
+ ```java
1149
+ class CreateUserCommandHandlerTest {
1150
+ private final UserRepositoryFake userRepository = new UserRepositoryFake();
1151
+ private final CreateUserCommandHandler handler =
1152
+ new CreateUserCommandHandler(userRepository);
1153
+
1154
+ @Test
1155
+ void shouldCreateUser() {
1156
+ CreateUserCommand command = new CreateUserCommand("john", "john@example.com");
1157
+
1158
+ String userId = handler.handle(command);
1159
+
1160
+ assertNotNull(userId);
1161
+ assertEquals(1, userRepository.count());
1162
+ }
1163
+ }
1164
+ ```
1165
+
1166
+ ### Estrategia por Capa
1167
+
1168
+ | Capa | Tipo de Test | Framework |
1169
+ |------|--------------|-----------|
1170
+ | Domain entities | Unidad pura | JUnit 5 |
1171
+ | Use cases | Unidad con Fakes | JUnit 5 + Fake repos |
1172
+ | Application mappers | Unidad | JUnit 5 |
1173
+ | Repository implementations | Integración | Testcontainers |
1174
+ | REST controllers | Integración | MockMvc |
1175
+
746
1176
  ---
747
1177
 
748
1178
  ## 📖 Documentos Relacionados
@@ -758,23 +1188,52 @@ void shouldValidateBusinessRules() {
758
1188
 
759
1189
  Al generar o modificar código, verificar:
760
1190
 
1191
+ **Entidades de Dominio:**
761
1192
  - [ ] Entidades de dominio **sin constructor vacío**
762
1193
  - [ ] Entidades de dominio **sin setters públicos**
763
1194
  - [ ] Métodos de negocio con **validaciones explícitas**
1195
+ - [ ] Value Objects **inmutables**
1196
+ - [ ] Sin anotaciones JSR-303 en entidades de dominio
1197
+
1198
+ **Entidades JPA:**
764
1199
  - [ ] Entidades JPA con **Lombok y herencia correcta**
1200
+ - [ ] `@SQLRestriction("deleted_at IS NULL")` cuando `hasSoftDelete: true`
1201
+ - [ ] No incluye campos de auditoría heredados en `@Builder`
1202
+
1203
+ **Mappers:**
765
1204
  - [ ] Mappers **excluyen campos de auditoría**
766
1205
  - [ ] Mappers **excluyen campos readOnly en creación**
767
1206
  - [ ] Mappers **excluyen campos hidden en respuestas**
1207
+ - [ ] Relaciones bidireccionales con métodos `assign*()`
1208
+
1209
+ **DTOs:**
768
1210
  - [ ] DTOs de respuesta **sin createdBy/updatedBy**
769
1211
  - [ ] DTOs de respuesta **sin campos hidden**
770
1212
  - [ ] DTOs de creación **sin campos readOnly**
771
- - [ ] Relaciones bidireccionales con métodos `assign*()`
772
- - [ ] Value Objects **inmutables**
773
- - [ ] Configuración de auditoría cuando `trackUser: true`
1213
+ - [ ] Usando Java Records
1214
+
1215
+ **Validaciones:**
774
1216
  - [ ] Validaciones JSR-303 **solo en Command y CreateDto, nunca en dominio**
1217
+ - [ ] `@Valid` en parámetros de endpoints REST
1218
+
1219
+ **Auditoría:**
1220
+ - [ ] Configuración de auditoría cuando `trackUser: true`
1221
+ - [ ] `@EnableJpaAuditing` con `auditorAwareRef = "auditorProvider"` en Application
1222
+
1223
+ **Soft Delete (cuando aplica):**
1224
+ - [ ] Usar `entity.softDelete()` + `repository.save()` — nunca `deleteById()`
1225
+ - [ ] `deletedAt` no expuesto en ResponseDto
1226
+
1227
+ **Características Avanzadas (cuando aplica):**
1228
+ - [ ] Enum con ciclo de vida → usar `transitions` + `initialValue`, no setters manuales
1229
+ - [ ] Value Object con comportamiento → declarar `methods` en lugar de lógica en entidad
1230
+ - [ ] Evento de dominio → declarar en `events[]`, publicar con `raise()` en método de negocio
1231
+ - [ ] Evento con broker → **no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
1232
+ - [ ] Distinguir entre Domain Event (`domain/models/events/X.java`) e Integration Event (`application/events/XIntegrationEvent.java`) — cambios de broker solo afectan al adaptador `MessageBroker`
1233
+ - [ ] Endpoints REST específicos → declarar `endpoints:` con versiones y operaciones; usar nombres estándar para implementación completa
775
1234
 
776
1235
  ---
777
1236
 
778
- **Última actualización:** 2026-02-21
779
- **Versión de eva4j:** 1.x
1237
+ **Última actualización:** 2026-03-11
1238
+ **Versión de eva4j:** 1.0.13
780
1239
  **Estado:** Documento de referencia para agentes IA