eva4j 1.0.12 → 1.0.13

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.
package/AGENTS.md CHANGED
@@ -451,11 +451,194 @@ 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
+ # kafka: true # opcional — publica a Kafka tras commit
454
461
  ```
455
462
 
456
463
  ---
457
464
 
458
- ## 🚨 Errores Comunes a Evitar
465
+ ## Características Avanzadas del domain.yaml
466
+
467
+ ### Value Objects con Métodos
468
+
469
+ Los Value Objects pueden declarar métodos de negocio directamente en `domain.yaml`:
470
+
471
+ ```yaml
472
+ valueObjects:
473
+ - name: Money
474
+ fields:
475
+ - name: amount
476
+ type: BigDecimal
477
+ - name: currency
478
+ type: String
479
+ methods:
480
+ - name: add
481
+ returnType: Money
482
+ parameters:
483
+ - name: other
484
+ type: Money
485
+ body: "return new Money(this.amount.add(other.getAmount()), this.currency);"
486
+ - name: isPositive
487
+ returnType: boolean
488
+ parameters: []
489
+ body: "return this.amount.compareTo(BigDecimal.ZERO) > 0;"
490
+ ```
491
+
492
+ ### Enums con Ciclo de Vida (Transitions)
493
+
494
+ Cuando un enum representa estados de negocio, declara `transitions` e `initialValue`:
495
+
496
+ ```yaml
497
+ enums:
498
+ - name: OrderStatus
499
+ initialValue: PENDING # Auto-inicializa en constructor; excluido del CreateDto
500
+ transitions:
501
+ - from: PENDING
502
+ to: CONFIRMED
503
+ method: confirm
504
+ - from: CONFIRMED
505
+ to: SHIPPED
506
+ method: ship
507
+ - from: [PENDING, CONFIRMED] # múltiples orígenes
508
+ to: CANCELLED
509
+ method: cancel
510
+ guard: "this.status == OrderStatus.DELIVERED" # lanza BusinessException si se cumple
511
+ values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
512
+ ```
513
+
514
+ Genera automáticamente **en la entidad raíz**: `confirm()`, `ship()`, `cancel()`, helpers `isPending()`, `isConfirmed()`, `canConfirm()`, `canCancel()`.
515
+ Genera automáticamente **en el enum**: `VALID_TRANSITIONS`, `canTransitionTo()`, `transitionTo()` (lanza `InvalidStateTransitionException` si la transición no es válida).
516
+
517
+ **Nota:** El campo con `initialValue` se trata como `readOnly: true` — no aparece en el constructor de negocio ni en el `CreateDto`.
518
+
519
+ ### Eventos de Dominio (`events[]`)
520
+
521
+ ```yaml
522
+ aggregates:
523
+ - name: Order
524
+ entities: [...]
525
+ events:
526
+ - name: OrderConfirmedEvent
527
+ fields:
528
+ - name: orderId
529
+ type: String
530
+ - name: confirmedAt
531
+ type: LocalDateTime
532
+ kafka: true # opcional — genera publicación a MessageBroker
533
+ ```
534
+
535
+ Genera `OrderConfirmedEvent.java` (en `domain/models/events/`) que extiende `DomainEvent`, y `OrderDomainEventHandler.java` (en `application/usecases/`) con `@TransactionalEventListener(AFTER_COMMIT)`.
536
+
537
+ Publicar desde la entidad raíz usando `raise()` heredado:
538
+
539
+ ```java
540
+ public void confirm() {
541
+ this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
542
+ raise(new OrderConfirmedEvent(this.id, this.id, LocalDateTime.now()));
543
+ }
544
+ ```
545
+
546
+ ---
547
+
548
+ ## �️ Soft Delete
549
+
550
+ Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera eliminación lógica en lugar de física.
551
+
552
+ ### Configuración en domain.yaml
553
+
554
+ ```yaml
555
+ entities:
556
+ - name: product
557
+ isRoot: true
558
+ tableName: products
559
+ hasSoftDelete: true # ✅ Activa soft delete
560
+ audit:
561
+ enabled: true
562
+ fields:
563
+ - name: id
564
+ type: String
565
+ - name: name
566
+ type: String
567
+ ```
568
+
569
+ ### Comportamiento generado
570
+
571
+ ```java
572
+ // Entidad JPA — filtrado automático con @SQLRestriction
573
+ @Entity
574
+ @SQLRestriction("deleted_at IS NULL")
575
+ public class ProductJpa extends AuditableEntity {
576
+ @Column(name = "deleted_at")
577
+ private LocalDateTime deletedAt;
578
+ }
579
+ ```
580
+
581
+ ```java
582
+ // Entidad de dominio — método de negocio
583
+ public class Product {
584
+ private LocalDateTime deletedAt;
585
+
586
+ public void softDelete() {
587
+ if (this.deletedAt != null) {
588
+ throw new IllegalStateException("Product is already deleted");
589
+ }
590
+ this.deletedAt = LocalDateTime.now();
591
+ }
592
+
593
+ public boolean isDeleted() {
594
+ return this.deletedAt != null;
595
+ }
596
+ }
597
+ ```
598
+
599
+ ### Reglas para Agentes
600
+
601
+ - **NUNCA** usar `repository.deleteById()` cuando hay soft delete
602
+ - **SIEMPRE** usar `entity.softDelete()` + `repository.save(entity)`
603
+ - **NUNCA** exponer `deletedAt` en ResponseDtos
604
+ - **SIEMPRE** usar `@SQLRestriction("deleted_at IS NULL")` en la entidad JPA
605
+
606
+ ---
607
+
608
+ ## ⏱️ Temporal Workflows
609
+
610
+ Cuando se agrega soporte de Temporal con `eva add temporal-client`, se genera infraestructura para workflows duraderos.
611
+
612
+ ### Archivos generados por `eva g temporal-flow <module>`
613
+
614
+ ```
615
+ [module]/
616
+ ├── application/workflows/
617
+ │ ├── OrderWorkflow.java # Interface (@WorkflowInterface)
618
+ │ └── OrderWorkflowImpl.java # Implementación (determinista)
619
+ └── infrastructure/temporal/
620
+ ├── activities/
621
+ │ ├── OrderActivity.java # Interface (@ActivityInterface)
622
+ │ └── OrderActivityImpl.java # Implementación (con I/O)
623
+ └── workers/
624
+ └── OrderWorker.java # Registro del worker
625
+ ```
626
+
627
+ ### Principios clave
628
+
629
+ - Los **Workflows deben ser deterministas** — sin `Math.random()`, `new Date()`, ni I/O
630
+ - Toda operación con efectos secundarios (DB, HTTP, emails) va en **Activities**
631
+ - Los **Use Cases** orquestan los workflows; las **Activities** ejecutan infraestructura
632
+ - Configuración de conexión en `resources/parameters/{env}/temporal.yaml`
633
+
634
+ ### Templates relacionados en eva4j
635
+
636
+ - `templates/temporal-flow/` — workflow interface e implementación
637
+ - `templates/temporal-activity/` — activity interface, implementación y worker
638
+
639
+ ---
640
+
641
+ ## �🚨 Errores Comunes a Evitar
459
642
 
460
643
  ### ❌ NO Crear Constructor Vacío en Dominio
461
644
 
@@ -566,6 +749,7 @@ Los campos en domain.yaml soportan las siguientes propiedades:
566
749
  | `enumValues` | Array | `[]` | Valores inline de enum |
567
750
  | **`readOnly`** | Boolean | `false` | **Excluye del constructor de negocio y CreateDto** |
568
751
  | **`hidden`** | Boolean | `false` | **Excluye del ResponseDto** |
752
+ | **`defaultValue`** | String/Number/Boolean | `null` | **Valor inicial en el constructor de creación (solo para `readOnly`)** |
569
753
  | **`validations`** | Array | `[]` | **Anotaciones JSR-303 en el Command y CreateDto** |
570
754
  | **`reference`** | Object | `null` | **Declara referencia semántica a otro agregado (genera comentario Javadoc)** |
571
755
 
@@ -601,9 +785,57 @@ fields:
601
785
  |-------|---------------------|-----------|-------------|
602
786
  | Normal | ✅ | ✅ | ✅ |
603
787
  | `readOnly: true` | ❌ | ❌ | ✅ |
788
+ | `readOnly` + `defaultValue` | ⚡ Asignado con default | ❌ | ✅ |
604
789
  | `hidden: true` | ✅ | ✅ | ❌ |
605
790
  | Ambos flags | ❌ | ❌ | ❌ |
606
791
 
792
+ #### 🎯 `defaultValue` - Valor Inicial para campos `readOnly`
793
+
794
+ 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.
795
+
796
+ ```yaml
797
+ fields:
798
+ - name: totalAmount
799
+ type: BigDecimal
800
+ readOnly: true
801
+ defaultValue: "0.00" # ✅ Acumulador inicializado
802
+
803
+ - name: status
804
+ type: OrderStatus
805
+ readOnly: true
806
+ defaultValue: PENDING # ✅ Estado inicial del enum
807
+
808
+ - name: itemCount
809
+ type: Integer
810
+ readOnly: true
811
+ defaultValue: 0 # ✅ Contador inicializado
812
+ ```
813
+
814
+ ```java
815
+ // Constructor de creación — defaultValues aplicados
816
+ public Order(String orderNumber, String customerId) {
817
+ this.orderNumber = orderNumber;
818
+ this.customerId = customerId;
819
+ this.totalAmount = new BigDecimal("0.00"); // ← defaultValue
820
+ this.status = OrderStatus.PENDING; // ← defaultValue
821
+ this.itemCount = 0; // ← defaultValue
822
+ }
823
+ ```
824
+
825
+ ```java
826
+ // JPA — @Builder.Default para respetar el valor en el builder
827
+ @Builder.Default
828
+ private BigDecimal totalAmount = new BigDecimal("0.00");
829
+
830
+ @Enumerated(EnumType.STRING)
831
+ @Builder.Default
832
+ private OrderStatus status = OrderStatus.PENDING;
833
+ ```
834
+
835
+ **Restricción:** `defaultValue` **solo aplica** a campos con `readOnly: true`. Usarlo en un campo no-readOnly genera un warning y es ignorado.
836
+
837
+ ---
838
+
607
839
  **Ejemplo práctico:**
608
840
  ```yaml
609
841
  entities:
@@ -626,6 +858,87 @@ entities:
626
858
  hidden: true
627
859
  ```
628
860
 
861
+ ### Validaciones JSR-303 (`validations`)
862
+
863
+ 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.
864
+
865
+ ```yaml
866
+ fields:
867
+ - name: email
868
+ type: String
869
+ validations:
870
+ - type: NotBlank
871
+ message: "Email es requerido"
872
+ - type: Email
873
+ message: "Email inválido"
874
+
875
+ - name: username
876
+ type: String
877
+ validations:
878
+ - type: Size
879
+ min: 3
880
+ max: 50
881
+ message: "Username entre 3 y 50 caracteres"
882
+
883
+ - name: age
884
+ type: Integer
885
+ validations:
886
+ - type: Min
887
+ value: 18
888
+ - type: Max
889
+ value: 120
890
+
891
+ - name: price
892
+ type: BigDecimal
893
+ validations:
894
+ - type: Positive
895
+ ```
896
+
897
+ **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`.
898
+
899
+ **Código generado en `CreateUserCommand.java`:**
900
+ ```java
901
+ public record CreateUserCommand(
902
+ @NotBlank(message = "Email es requerido")
903
+ @Email(message = "Email inválido")
904
+ String email,
905
+
906
+ @Size(min = 3, max = 50, message = "Username entre 3 y 50 caracteres")
907
+ String username
908
+ ) implements Command {}
909
+ ```
910
+
911
+ ### Referencias entre Agregados (`reference`)
912
+
913
+ 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).
914
+
915
+ ```yaml
916
+ fields:
917
+ - name: customerId
918
+ type: String
919
+ reference:
920
+ aggregate: Customer # Nombre del agregado referenciado (PascalCase)
921
+ module: customers # Módulo donde vive (opcional si es el mismo módulo)
922
+
923
+ - name: productId
924
+ type: String
925
+ reference:
926
+ aggregate: Product
927
+ module: catalog
928
+ ```
929
+
930
+ **Código generado:**
931
+ ```java
932
+ // En Order.java (domain entity)
933
+ /** Cross-aggregate reference → Customer (module: customers) */
934
+ private String customerId;
935
+
936
+ // En OrderJpa.java
937
+ @Column(name = "customer_id")
938
+ /** Cross-aggregate reference → Customer (module: customers) */
939
+ private String customerId;
940
+ ```
941
+
629
942
  ### Tipos de Relaciones
630
943
 
631
944
  - `OneToOne` - Relación uno a uno
@@ -637,6 +950,14 @@ entities:
637
950
 
638
951
  ## 🎯 Mejores Prácticas para Agentes
639
952
 
953
+ ### Al Generar domain.yaml (Flujo SDD)
954
+
955
+ 1. **SIEMPRE** incluir campo `id` en todas las entidades
956
+ 2. **SI** el módulo requiere ciclo de vida → usar `transitions` + `initialValue` en el enum
957
+ 3. **SI** un valor tiene lógica de negocio → declararlo como `valueObject` con `methods`
958
+ 4. **SI** ocurren hechos relevantes de negocio → declarar `events[]` en el agregado
959
+ 5. **DESPUÉS** de generar el `domain.yaml` → ejecutar `eva g entities <module>`
960
+
640
961
  ### Al Generar Código de Dominio
641
962
 
642
963
  1. **NUNCA** crear constructor vacío en entidades de dominio
@@ -722,13 +1043,13 @@ HTTP Response (sin createdBy/updatedBy)
722
1043
 
723
1044
  ## 🧪 Testing
724
1045
 
725
- ### Tests de Dominio
1046
+ ### Tests de Dominio (Unidad Pura)
726
1047
 
727
1048
  ```java
728
1049
  @Test
729
1050
  void shouldCreateUserWithValidData() {
730
1051
  User user = new User("john", "john@example.com");
731
-
1052
+
732
1053
  assertEquals("john", user.getUsername());
733
1054
  assertEquals("john@example.com", user.getEmail());
734
1055
  }
@@ -736,13 +1057,82 @@ void shouldCreateUserWithValidData() {
736
1057
  @Test
737
1058
  void shouldValidateBusinessRules() {
738
1059
  User user = new User("john", "john@example.com");
739
-
1060
+
740
1061
  assertThrows(IllegalArgumentException.class, () -> {
741
1062
  user.changeEmail("invalid-email");
742
1063
  });
743
1064
  }
744
1065
  ```
745
1066
 
1067
+ ### Object Mother Pattern
1068
+
1069
+ ```java
1070
+ // src/test/java/[package]/user/domain/UserMother.java
1071
+ public class UserMother {
1072
+
1073
+ public static User valid() {
1074
+ return new User("john_doe", "john@example.com");
1075
+ }
1076
+
1077
+ public static User withEmail(String email) {
1078
+ return new User("john_doe", email);
1079
+ }
1080
+ }
1081
+ ```
1082
+
1083
+ ### Repositorio Fake (In-Memory)
1084
+
1085
+ Para testear Use Cases sin base de datos:
1086
+
1087
+ ```java
1088
+ public class UserRepositoryFake implements UserRepository {
1089
+ private final Map<String, User> store = new HashMap<>();
1090
+
1091
+ @Override
1092
+ public User save(User user) {
1093
+ store.put(user.getId(), user);
1094
+ return user;
1095
+ }
1096
+
1097
+ @Override
1098
+ public Optional<User> findById(String id) {
1099
+ return Optional.ofNullable(store.get(id));
1100
+ }
1101
+
1102
+ public int count() { return store.size(); }
1103
+ }
1104
+ ```
1105
+
1106
+ ### Tests de Use Cases
1107
+
1108
+ ```java
1109
+ class CreateUserCommandHandlerTest {
1110
+ private final UserRepositoryFake userRepository = new UserRepositoryFake();
1111
+ private final CreateUserCommandHandler handler =
1112
+ new CreateUserCommandHandler(userRepository);
1113
+
1114
+ @Test
1115
+ void shouldCreateUser() {
1116
+ CreateUserCommand command = new CreateUserCommand("john", "john@example.com");
1117
+
1118
+ String userId = handler.handle(command);
1119
+
1120
+ assertNotNull(userId);
1121
+ assertEquals(1, userRepository.count());
1122
+ }
1123
+ }
1124
+ ```
1125
+
1126
+ ### Estrategia por Capa
1127
+
1128
+ | Capa | Tipo de Test | Framework |
1129
+ |------|--------------|-----------|
1130
+ | Domain entities | Unidad pura | JUnit 5 |
1131
+ | Use cases | Unidad con Fakes | JUnit 5 + Fake repos |
1132
+ | Application mappers | Unidad | JUnit 5 |
1133
+ | Repository implementations | Integración | Testcontainers |
1134
+ | REST controllers | Integración | MockMvc |
1135
+
746
1136
  ---
747
1137
 
748
1138
  ## 📖 Documentos Relacionados
@@ -758,23 +1148,50 @@ void shouldValidateBusinessRules() {
758
1148
 
759
1149
  Al generar o modificar código, verificar:
760
1150
 
1151
+ **Entidades de Dominio:**
761
1152
  - [ ] Entidades de dominio **sin constructor vacío**
762
1153
  - [ ] Entidades de dominio **sin setters públicos**
763
1154
  - [ ] Métodos de negocio con **validaciones explícitas**
1155
+ - [ ] Value Objects **inmutables**
1156
+ - [ ] Sin anotaciones JSR-303 en entidades de dominio
1157
+
1158
+ **Entidades JPA:**
764
1159
  - [ ] Entidades JPA con **Lombok y herencia correcta**
1160
+ - [ ] `@SQLRestriction("deleted_at IS NULL")` cuando `hasSoftDelete: true`
1161
+ - [ ] No incluye campos de auditoría heredados en `@Builder`
1162
+
1163
+ **Mappers:**
765
1164
  - [ ] Mappers **excluyen campos de auditoría**
766
1165
  - [ ] Mappers **excluyen campos readOnly en creación**
767
1166
  - [ ] Mappers **excluyen campos hidden en respuestas**
1167
+ - [ ] Relaciones bidireccionales con métodos `assign*()`
1168
+
1169
+ **DTOs:**
768
1170
  - [ ] DTOs de respuesta **sin createdBy/updatedBy**
769
1171
  - [ ] DTOs de respuesta **sin campos hidden**
770
1172
  - [ ] 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`
1173
+ - [ ] Usando Java Records
1174
+
1175
+ **Validaciones:**
774
1176
  - [ ] Validaciones JSR-303 **solo en Command y CreateDto, nunca en dominio**
1177
+ - [ ] `@Valid` en parámetros de endpoints REST
1178
+
1179
+ **Auditoría:**
1180
+ - [ ] Configuración de auditoría cuando `trackUser: true`
1181
+ - [ ] `@EnableJpaAuditing` con `auditorAwareRef = "auditorProvider"` en Application
1182
+
1183
+ **Soft Delete (cuando aplica):**
1184
+ - [ ] Usar `entity.softDelete()` + `repository.save()` — nunca `deleteById()`
1185
+ - [ ] `deletedAt` no expuesto en ResponseDto
1186
+
1187
+ **Características Avanzadas (cuando aplica):**
1188
+ - [ ] Enum con ciclo de vida → usar `transitions` + `initialValue`, no setters manuales
1189
+ - [ ] Value Object con comportamiento → declarar `methods` en lugar de lógica en entidad
1190
+ - [ ] Evento de dominio → declarar en `events[]`, publicar con `raise()` en método de negocio
1191
+ - [ ] Evento con Kafka → agregar `kafka: true` al evento
775
1192
 
776
1193
  ---
777
1194
 
778
- **Última actualización:** 2026-02-21
779
- **Versión de eva4j:** 1.x
1195
+ **Última actualización:** 2026-03-02
1196
+ **Versión de eva4j:** 1.0.12
780
1197
  **Estado:** Documento de referencia para agentes IA