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 +426 -9
- package/DOMAIN_YAML_GUIDE.md +374 -21
- package/FUTURE_FEATURES.md +94 -1
- package/docs/commands/GENERATE_ENTITIES.md +252 -2404
- package/docs/commands/GENERATE_RECORD.md +335 -311
- package/examples/doctor-evaluation.yaml +3 -3
- package/examples/domain-audit-complete.yaml +2 -2
- package/examples/domain-collections.yaml +2 -2
- package/examples/domain-ecommerce.yaml +2 -2
- package/examples/domain-events.yaml +1 -1
- package/examples/domain-field-visibility.yaml +11 -5
- package/examples/domain-multi-aggregate.yaml +6 -6
- package/examples/domain-one-to-many.yaml +1 -1
- package/examples/domain-one-to-one.yaml +1 -1
- package/examples/domain-secondary-onetomany.yaml +1 -1
- package/examples/domain-secondary-onetoone.yaml +1 -1
- package/examples/domain-simple.yaml +1 -1
- package/examples/domain-soft-delete.yaml +3 -3
- package/examples/domain-transitions.yaml +1 -1
- package/examples/domain-value-objects.yaml +1 -1
- package/package.json +1 -1
- package/src/utils/yaml-to-entity.js +69 -2
- package/templates/aggregate/AggregateRoot.java.ejs +5 -1
- package/templates/aggregate/DomainEntity.java.ejs +5 -1
- package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -1
- package/templates/aggregate/JpaEntity.java.ejs +2 -1
- package/templates/base/root/AGENTS.md.ejs +916 -51
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
|
-
##
|
|
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
|
-
- [ ]
|
|
772
|
-
|
|
773
|
-
|
|
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
|
|
779
|
-
**Versión de eva4j:** 1.
|
|
1195
|
+
**Última actualización:** 2026-03-02
|
|
1196
|
+
**Versión de eva4j:** 1.0.12
|
|
780
1197
|
**Estado:** Documento de referencia para agentes IA
|