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
|
@@ -73,7 +73,7 @@ eva4j genera proyectos Spring Boot siguiendo **arquitectura hexagonal (puertos y
|
|
|
73
73
|
│ │ ├── <%= applicationClassName %>.java # Spring Boot entry point
|
|
74
74
|
│ │ ├── common/ # Infrastructure (config, exceptions, security)
|
|
75
75
|
│ │ ├── shared/ # Cross-cutting domain (base entities, events)
|
|
76
|
-
│ │ └── [modules]/ # Business modules (after `
|
|
76
|
+
│ │ └── [modules]/ # Business modules (after `eva add module`)
|
|
77
77
|
│ │ └── user/ # Example module
|
|
78
78
|
│ │ ├── domain/
|
|
79
79
|
│ │ │ ├── models/
|
|
@@ -119,7 +119,7 @@ eva4j genera proyectos Spring Boot siguiendo **arquitectura hexagonal (puertos y
|
|
|
119
119
|
|
|
120
120
|
Each module (e.g., `user`, `order`, `product`) is:
|
|
121
121
|
- **Self-contained**: Own domain, application, and infrastructure layers
|
|
122
|
-
- **Independent**: Can be extracted to microservice with `
|
|
122
|
+
- **Independent**: Can be extracted to microservice with `eva detach`
|
|
123
123
|
- **Bounded Context**: Represents a business capability
|
|
124
124
|
- **Spring Modulith Module**: Boundaries enforced at runtime
|
|
125
125
|
|
|
@@ -260,7 +260,7 @@ The `domain.yaml` file is the **single source of truth** for your domain model.
|
|
|
260
260
|
- JPA entities (with Lombok, @Entity, @Table)
|
|
261
261
|
- Repository interfaces and implementations
|
|
262
262
|
- Mappers (bidirectional domain ↔ JPA)
|
|
263
|
-
- CRUD operations (when using `
|
|
263
|
+
- CRUD operations (when using `eva g resource`)
|
|
264
264
|
|
|
265
265
|
### Basic Structure
|
|
266
266
|
|
|
@@ -312,12 +312,29 @@ aggregates:
|
|
|
312
312
|
|
|
313
313
|
enums: # Enumerations
|
|
314
314
|
- name: OrderStatus
|
|
315
|
+
initialValue: DRAFT # Optional: auto-initializes field; excluded from CreateDto
|
|
316
|
+
transitions: # Optional: generates transition methods on root entity
|
|
317
|
+
- from: DRAFT
|
|
318
|
+
to: CONFIRMED
|
|
319
|
+
method: confirm
|
|
320
|
+
- from: [DRAFT, CONFIRMED]
|
|
321
|
+
to: CANCELLED
|
|
322
|
+
method: cancel
|
|
315
323
|
values:
|
|
316
324
|
- DRAFT
|
|
317
325
|
- CONFIRMED
|
|
318
326
|
- SHIPPED
|
|
319
327
|
- DELIVERED
|
|
320
328
|
- CANCELLED
|
|
329
|
+
|
|
330
|
+
events: # Domain Events emitted by this aggregate
|
|
331
|
+
- name: OrderConfirmedEvent
|
|
332
|
+
fields:
|
|
333
|
+
- name: orderId
|
|
334
|
+
type: String
|
|
335
|
+
- name: confirmedAt
|
|
336
|
+
type: LocalDateTime
|
|
337
|
+
# kafka: true # Optional — publishes to Kafka via MessageBroker
|
|
321
338
|
```
|
|
322
339
|
|
|
323
340
|
### Supported Field Types
|
|
@@ -336,6 +353,110 @@ aggregates:
|
|
|
336
353
|
| `EnumName` | `EnumName` | Reference to enum in aggregate |
|
|
337
354
|
| `ValueObjName` | `ValueObjName` | Reference to value object |
|
|
338
355
|
|
|
356
|
+
### Field Properties
|
|
357
|
+
|
|
358
|
+
Each field in `domain.yaml` supports the following properties:
|
|
359
|
+
|
|
360
|
+
| Propiedad | Tipo | Default | Descripción |
|
|
361
|
+
|-----------|------|---------|-------------|
|
|
362
|
+
| `name` | String | — | Nombre del campo (obligatorio) |
|
|
363
|
+
| `type` | String | — | Tipo de dato Java (obligatorio) |
|
|
364
|
+
| `annotations` | Array | `[]` | Anotaciones JPA personalizadas |
|
|
365
|
+
| `isValueObject` | Boolean | `false` | Marca explícita de Value Object |
|
|
366
|
+
| `isEmbedded` | Boolean | `false` | Marca explícita de `@Embedded` |
|
|
367
|
+
| `enumValues` | Array | `[]` | Valores inline de enum |
|
|
368
|
+
| `readOnly` | Boolean | `false` | Excluye del constructor de negocio y CreateDto |
|
|
369
|
+
| `hidden` | Boolean | `false` | Excluye del ResponseDto |
|
|
370
|
+
| `defaultValue` | String/Number/Boolean | `null` | Valor inicial en el constructor de creación (solo para `readOnly`) |
|
|
371
|
+
| `validations` | Array | `[]` | Anotaciones JSR-303 aplicadas en Command y CreateDto |
|
|
372
|
+
| `reference` | Object | `null` | Referencia semántica a otro agregado (genera Javadoc) |
|
|
373
|
+
|
|
374
|
+
#### Flags de Visibilidad: `readOnly` y `hidden`
|
|
375
|
+
|
|
376
|
+
**`readOnly: true`** — Campos calculados/derivados:
|
|
377
|
+
- ❌ Excluido de: constructor de negocio, CreateDto
|
|
378
|
+
- ✅ Incluido en: constructor completo (reconstrucción), ResponseDto
|
|
379
|
+
|
|
380
|
+
**`hidden: true`** — Campos sensibles/internos:
|
|
381
|
+
- ❌ Excluido de: ResponseDto
|
|
382
|
+
- ✅ Incluido en: constructor de negocio, CreateDto
|
|
383
|
+
|
|
384
|
+
**Matriz de comportamiento:**
|
|
385
|
+
|
|
386
|
+
| Campo | Constructor Negocio | CreateDto | ResponseDto |
|
|
387
|
+
|-------|:-------------------:|:---------:|:-----------:|
|
|
388
|
+
| Normal | ✅ | ✅ | ✅ |
|
|
389
|
+
| `readOnly: true` | ❌ | ❌ | ✅ |
|
|
390
|
+
| `readOnly` + `defaultValue` | ⚡ Asignado con default | ❌ | ✅ |
|
|
391
|
+
| `hidden: true` | ✅ | ✅ | ❌ |
|
|
392
|
+
| `readOnly` + `hidden` | ❌ | ❌ | ❌ |
|
|
393
|
+
|
|
394
|
+
#### `defaultValue` - Valor Inicial para campos `readOnly`
|
|
395
|
+
|
|
396
|
+
Cuando un campo `readOnly` tiene un valor inicial predecible, se puede declarar con `defaultValue`. El generador emite la asignación en el **constructor de creación** del dominio y `@Builder.Default` en la entidad JPA.
|
|
397
|
+
|
|
398
|
+
```yaml
|
|
399
|
+
fields:
|
|
400
|
+
- name: totalAmount
|
|
401
|
+
type: BigDecimal
|
|
402
|
+
readOnly: true
|
|
403
|
+
defaultValue: "0.00" # ✅ Acumulador inicializado
|
|
404
|
+
|
|
405
|
+
- name: status
|
|
406
|
+
type: OrderStatus
|
|
407
|
+
readOnly: true
|
|
408
|
+
defaultValue: PENDING # ✅ Estado inicial del enum
|
|
409
|
+
|
|
410
|
+
- name: itemCount
|
|
411
|
+
type: Integer
|
|
412
|
+
readOnly: true
|
|
413
|
+
defaultValue: 0 # ✅ Contador inicializado
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
```java
|
|
417
|
+
// Constructor de creación — defaultValues aplicados automáticamente
|
|
418
|
+
public Order(String orderNumber, String customerId) {
|
|
419
|
+
this.orderNumber = orderNumber;
|
|
420
|
+
this.customerId = customerId;
|
|
421
|
+
this.totalAmount = new BigDecimal("0.00"); // ← defaultValue
|
|
422
|
+
this.status = OrderStatus.PENDING; // ← defaultValue
|
|
423
|
+
this.itemCount = 0; // ← defaultValue
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
> **Restricción:** `defaultValue` **solo aplica** a campos con `readOnly: true`. Usarlo en un campo no-readOnly genera un warning y es ignorado.
|
|
428
|
+
|
|
429
|
+
```yaml
|
|
430
|
+
# Ejemplo completo con todos los flags
|
|
431
|
+
entities:
|
|
432
|
+
- name: order
|
|
433
|
+
fields:
|
|
434
|
+
- name: orderNumber
|
|
435
|
+
type: String # Normal - aparece en todos lados
|
|
436
|
+
|
|
437
|
+
- name: totalAmount
|
|
438
|
+
type: BigDecimal
|
|
439
|
+
readOnly: true # Calculado - no en constructor ni CreateDto
|
|
440
|
+
|
|
441
|
+
- name: processingToken
|
|
442
|
+
type: String
|
|
443
|
+
hidden: true # Sensible - no en ResponseDto
|
|
444
|
+
|
|
445
|
+
- name: customerEmail
|
|
446
|
+
type: String
|
|
447
|
+
validations:
|
|
448
|
+
- "@NotBlank"
|
|
449
|
+
- "@Email" # Validaciones JSR-303 en Command y CreateDto
|
|
450
|
+
|
|
451
|
+
- name: externalOrderId
|
|
452
|
+
type: String
|
|
453
|
+
reference:
|
|
454
|
+
aggregate: ExternalSystem # Genera Javadoc de referencia semántica
|
|
455
|
+
description: "ID del pedido en sistema externo"
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
> **⚠️ IMPORTANTE**: Las anotaciones `validations` se aplican **SOLO** en los Commands y CreateDtos — **NUNCA** en las entidades de dominio.
|
|
459
|
+
|
|
339
460
|
### Relationships
|
|
340
461
|
|
|
341
462
|
> **⚠️ IMPORTANT**: eva4j **automatically generates inverse relationships**. For bidirectional relationships (OneToMany, OneToOne), you only need to define the relationship **on the parent side** with `mappedBy`. The inverse relationship (ManyToOne) is automatically created. You do NOT need to define both directions manually.
|
|
@@ -689,56 +810,68 @@ public enum OrderStatus {
|
|
|
689
810
|
### Project Setup
|
|
690
811
|
```bash
|
|
691
812
|
# Create new project
|
|
692
|
-
|
|
813
|
+
eva create my-project
|
|
693
814
|
|
|
694
815
|
# Add a new module
|
|
695
|
-
|
|
816
|
+
eva add module order
|
|
696
817
|
|
|
697
818
|
# View project configuration
|
|
698
|
-
|
|
819
|
+
eva info
|
|
699
820
|
```
|
|
700
821
|
|
|
701
822
|
### Code Generation (must have domain.yaml)
|
|
702
823
|
```bash
|
|
703
824
|
# Generate entities from domain.yaml
|
|
704
|
-
|
|
705
|
-
|
|
825
|
+
eva generate entities order
|
|
826
|
+
eva g entities order # Alias
|
|
706
827
|
|
|
707
828
|
# Generate CRUD REST API
|
|
708
|
-
|
|
829
|
+
eva g resource order # Creates controller + DTOs + CRUD handlers
|
|
709
830
|
|
|
710
831
|
# Generate custom use case
|
|
711
|
-
|
|
832
|
+
eva g usecase order CreateCustomOrder
|
|
712
833
|
```
|
|
713
834
|
|
|
714
835
|
### Event-Driven Features
|
|
715
836
|
```bash
|
|
716
837
|
# Add Kafka client
|
|
717
|
-
|
|
838
|
+
eva add kafka-client
|
|
718
839
|
|
|
719
840
|
# Publish events
|
|
720
|
-
|
|
841
|
+
eva g kafka-event order order-created
|
|
721
842
|
|
|
722
843
|
# Consume events
|
|
723
|
-
|
|
844
|
+
eva g kafka-listener notification
|
|
724
845
|
```
|
|
725
846
|
|
|
726
847
|
### External API Integration
|
|
727
848
|
```bash
|
|
728
849
|
# Add HTTP client for external service
|
|
729
|
-
|
|
850
|
+
eva g http-exchange order payment-service
|
|
730
851
|
```
|
|
731
852
|
|
|
732
853
|
### Record Generation
|
|
733
854
|
```bash
|
|
734
855
|
# Generate record from JSON (clipboard or --json)
|
|
735
|
-
|
|
856
|
+
eva g record
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
### Temporal Workflows
|
|
860
|
+
```bash
|
|
861
|
+
# Add Temporal workflow engine dependency
|
|
862
|
+
eva add temporal-client
|
|
863
|
+
|
|
864
|
+
# Generate workflow definition
|
|
865
|
+
eva g temporal-flow order
|
|
866
|
+
|
|
867
|
+
# Generate activity implementation
|
|
868
|
+
eva g temporal-activity order
|
|
736
869
|
```
|
|
737
870
|
|
|
738
871
|
### Microservice Extraction
|
|
739
872
|
```bash
|
|
740
873
|
# Extract module to independent microservice
|
|
741
|
-
|
|
874
|
+
eva detach order # Creates ../order_msvc/ project
|
|
742
875
|
```
|
|
743
876
|
|
|
744
877
|
---
|
|
@@ -855,6 +988,51 @@ public OrderJpa toJpa(Order domain) {
|
|
|
855
988
|
|
|
856
989
|
**Razón:** Los campos de auditoría son heredados de clases base y JPA Auditing los popula automáticamente.
|
|
857
990
|
|
|
991
|
+
### Mappers - Exclusión de campos `readOnly` y `hidden`
|
|
992
|
+
|
|
993
|
+
**En métodos de creación** (`fromCommand`, `fromDto`) — excluir campos `readOnly`:
|
|
994
|
+
```java
|
|
995
|
+
// ✅ CORRECTO - fromCommand NO incluye campos readOnly
|
|
996
|
+
public Order fromCommand(CreateOrderCommand command) {
|
|
997
|
+
return new Order(
|
|
998
|
+
command.customerId(),
|
|
999
|
+
command.orderDate()
|
|
1000
|
+
// totalAmount NO se incluye: es readOnly (calculado)
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// ❌ INCORRECTO - readOnly en constructor de negocio
|
|
1005
|
+
public Order fromCommand(CreateOrderCommand command) {
|
|
1006
|
+
return new Order(
|
|
1007
|
+
command.customerId(),
|
|
1008
|
+
command.orderDate(),
|
|
1009
|
+
command.totalAmount() // ❌ totalAmount es readOnly
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
**En métodos de respuesta** (`toDto`, `toResponseDto`) — excluir campos `hidden`:
|
|
1015
|
+
```java
|
|
1016
|
+
// ✅ CORRECTO - toDto NO incluye campos hidden
|
|
1017
|
+
public OrderResponseDto toDto(Order domain) {
|
|
1018
|
+
return new OrderResponseDto(
|
|
1019
|
+
domain.getId(),
|
|
1020
|
+
domain.getOrderNumber(),
|
|
1021
|
+
domain.getTotalAmount()
|
|
1022
|
+
// processingToken NO se incluye: es hidden (sensible)
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// ❌ INCORRECTO - hidden en ResponseDto
|
|
1027
|
+
public OrderResponseDto toDto(Order domain) {
|
|
1028
|
+
return new OrderResponseDto(
|
|
1029
|
+
domain.getId(),
|
|
1030
|
+
domain.getOrderNumber(),
|
|
1031
|
+
domain.getProcessingToken() // ❌ processingToken es hidden
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
```
|
|
1035
|
+
|
|
858
1036
|
### Filtro de Campos en Templates
|
|
859
1037
|
|
|
860
1038
|
Los templates excluyen automáticamente campos de auditoría al generar mappers:
|
|
@@ -928,7 +1106,7 @@ aggregates:
|
|
|
928
1106
|
|
|
929
1107
|
**Regenerate**:
|
|
930
1108
|
```bash
|
|
931
|
-
|
|
1109
|
+
eva g entities order
|
|
932
1110
|
```
|
|
933
1111
|
|
|
934
1112
|
### 2. Adding a New Enum
|
|
@@ -950,13 +1128,13 @@ aggregates:
|
|
|
950
1128
|
|
|
951
1129
|
**Regenerate**:
|
|
952
1130
|
```bash
|
|
953
|
-
|
|
1131
|
+
eva g entities order
|
|
954
1132
|
```
|
|
955
1133
|
|
|
956
1134
|
### 3. Adding a Custom Use Case
|
|
957
1135
|
|
|
958
1136
|
```bash
|
|
959
|
-
|
|
1137
|
+
eva g usecase order CancelOrder
|
|
960
1138
|
```
|
|
961
1139
|
|
|
962
1140
|
Creates:
|
|
@@ -989,7 +1167,7 @@ public class CancelOrderCommandHandler implements CommandHandler<CancelOrderComm
|
|
|
989
1167
|
### 4. Adding Event Publishing
|
|
990
1168
|
|
|
991
1169
|
```bash
|
|
992
|
-
|
|
1170
|
+
eva g kafka-event order order-cancelled
|
|
993
1171
|
```
|
|
994
1172
|
|
|
995
1173
|
**Publish event** in use case:
|
|
@@ -1030,7 +1208,7 @@ public class CancelOrderCommandHandler implements CommandHandler<CancelOrderComm
|
|
|
1030
1208
|
### 5. Adding Event Listener
|
|
1031
1209
|
|
|
1032
1210
|
```bash
|
|
1033
|
-
|
|
1211
|
+
eva g kafka-listener notification
|
|
1034
1212
|
# Select topics: order-cancelled
|
|
1035
1213
|
```
|
|
1036
1214
|
|
|
@@ -1161,6 +1339,257 @@ relationships:
|
|
|
1161
1339
|
- `ManyToOne` - Relación muchos a uno
|
|
1162
1340
|
- `ManyToMany` - Relación muchos a muchos (evitar si es posible)
|
|
1163
1341
|
|
|
1342
|
+
### Validaciones JSR-303 (`validations`)
|
|
1343
|
+
|
|
1344
|
+
Se declaran en el campo y se aplican **únicamente** en el `Command` y `CreateDto`. **Nunca** en entidades de dominio.
|
|
1345
|
+
|
|
1346
|
+
```yaml
|
|
1347
|
+
fields:
|
|
1348
|
+
- name: email
|
|
1349
|
+
type: String
|
|
1350
|
+
validations:
|
|
1351
|
+
- type: NotBlank
|
|
1352
|
+
message: "Email es requerido"
|
|
1353
|
+
- type: Email
|
|
1354
|
+
message: "Email inválido"
|
|
1355
|
+
|
|
1356
|
+
- name: username
|
|
1357
|
+
type: String
|
|
1358
|
+
validations:
|
|
1359
|
+
- type: Size
|
|
1360
|
+
min: 3
|
|
1361
|
+
max: 50
|
|
1362
|
+
|
|
1363
|
+
- name: age
|
|
1364
|
+
type: Integer
|
|
1365
|
+
validations:
|
|
1366
|
+
- type: Min
|
|
1367
|
+
value: 18
|
|
1368
|
+
|
|
1369
|
+
- name: price
|
|
1370
|
+
type: BigDecimal
|
|
1371
|
+
validations:
|
|
1372
|
+
- type: Positive
|
|
1373
|
+
|
|
1374
|
+
- name: code
|
|
1375
|
+
type: String
|
|
1376
|
+
validations:
|
|
1377
|
+
- type: Pattern
|
|
1378
|
+
regexp: "^[A-Z]{3}-[0-9]{4}$"
|
|
1379
|
+
message: "Formato inválido"
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
**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`.
|
|
1383
|
+
|
|
1384
|
+
**Código generado en `Create<Aggregate>Command.java`:**
|
|
1385
|
+
```java
|
|
1386
|
+
public record Create<%= applicationClassName.replace('Application','') %>Command(
|
|
1387
|
+
@NotBlank(message = "Email es requerido")
|
|
1388
|
+
@Email(message = "Email inválido")
|
|
1389
|
+
String email,
|
|
1390
|
+
|
|
1391
|
+
@Size(min = 3, max = 50)
|
|
1392
|
+
String username
|
|
1393
|
+
) implements Command {}
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
### Referencias entre Agregados (`reference`)
|
|
1397
|
+
|
|
1398
|
+
Declara que un campo es un ID de otro agregado. El tipo Java **no cambia** — sigue siendo `String`, `Long`, etc. — pero genera un comentario Javadoc. **No genera `@ManyToOne`** (correcto en DDD).
|
|
1399
|
+
|
|
1400
|
+
```yaml
|
|
1401
|
+
fields:
|
|
1402
|
+
- name: customerId
|
|
1403
|
+
type: String
|
|
1404
|
+
reference:
|
|
1405
|
+
aggregate: Customer # Nombre del agregado referenciado (PascalCase)
|
|
1406
|
+
module: customers # Módulo donde vive (opcional si es el mismo)
|
|
1407
|
+
|
|
1408
|
+
- name: productId
|
|
1409
|
+
type: String
|
|
1410
|
+
reference:
|
|
1411
|
+
aggregate: Product
|
|
1412
|
+
module: catalog
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
**Código generado:**
|
|
1416
|
+
```java
|
|
1417
|
+
// En Order.java (domain entity)
|
|
1418
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
1419
|
+
private String customerId;
|
|
1420
|
+
|
|
1421
|
+
// En OrderJpa.java
|
|
1422
|
+
@Column(name = "customer_id")
|
|
1423
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
1424
|
+
private String customerId;
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
### Value Objects con Métodos de Negocio
|
|
1428
|
+
|
|
1429
|
+
Los Value Objects pueden declarar métodos directamente en `domain.yaml`:
|
|
1430
|
+
|
|
1431
|
+
```yaml
|
|
1432
|
+
valueObjects:
|
|
1433
|
+
- name: Money
|
|
1434
|
+
fields:
|
|
1435
|
+
- name: amount
|
|
1436
|
+
type: BigDecimal
|
|
1437
|
+
- name: currency
|
|
1438
|
+
type: String
|
|
1439
|
+
methods:
|
|
1440
|
+
- name: add
|
|
1441
|
+
returnType: Money
|
|
1442
|
+
parameters:
|
|
1443
|
+
- name: other
|
|
1444
|
+
type: Money
|
|
1445
|
+
body: "return new Money(this.amount.add(other.getAmount()), this.currency);"
|
|
1446
|
+
- name: isPositive
|
|
1447
|
+
returnType: boolean
|
|
1448
|
+
parameters: []
|
|
1449
|
+
body: "return this.amount.compareTo(BigDecimal.ZERO) > 0;"
|
|
1450
|
+
```
|
|
1451
|
+
|
|
1452
|
+
Genera en `Money.java`:
|
|
1453
|
+
```java
|
|
1454
|
+
public Money add(Money other) {
|
|
1455
|
+
return new Money(this.amount.add(other.getAmount()), this.currency);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
public boolean isPositive() {
|
|
1459
|
+
return this.amount.compareTo(BigDecimal.ZERO) > 0;
|
|
1460
|
+
}
|
|
1461
|
+
```
|
|
1462
|
+
|
|
1463
|
+
### Enums con Ciclo de Vida (Transitions)
|
|
1464
|
+
|
|
1465
|
+
Cuando un enum representa un ciclo de vida de negocio, declara `transitions` e `initialValue`:
|
|
1466
|
+
|
|
1467
|
+
```yaml
|
|
1468
|
+
enums:
|
|
1469
|
+
- name: OrderStatus
|
|
1470
|
+
initialValue: PENDING # Auto-inicializa en constructor; campo tratado como readOnly
|
|
1471
|
+
transitions:
|
|
1472
|
+
- from: PENDING
|
|
1473
|
+
to: CONFIRMED
|
|
1474
|
+
method: confirm
|
|
1475
|
+
- from: CONFIRMED
|
|
1476
|
+
to: SHIPPED
|
|
1477
|
+
method: ship
|
|
1478
|
+
- from: SHIPPED
|
|
1479
|
+
to: DELIVERED
|
|
1480
|
+
method: deliver
|
|
1481
|
+
- from: [PENDING, CONFIRMED] # múltiples orígenes
|
|
1482
|
+
to: CANCELLED
|
|
1483
|
+
method: cancel
|
|
1484
|
+
guard: "this.status == OrderStatus.DELIVERED" # lanza BusinessException si se cumple
|
|
1485
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
1486
|
+
```
|
|
1487
|
+
|
|
1488
|
+
**En el enum generado (`OrderStatus.java`):**
|
|
1489
|
+
```java
|
|
1490
|
+
public enum OrderStatus {
|
|
1491
|
+
PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED;
|
|
1492
|
+
|
|
1493
|
+
private static final Map<OrderStatus, Set<OrderStatus>> VALID_TRANSITIONS = ...;
|
|
1494
|
+
|
|
1495
|
+
public boolean canTransitionTo(OrderStatus target) { ... }
|
|
1496
|
+
public OrderStatus transitionTo(OrderStatus target) { ... } // throws InvalidStateTransitionException
|
|
1497
|
+
}
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
**En la entidad raíz (`Order.java`):**
|
|
1501
|
+
```java
|
|
1502
|
+
// Constructor de creación — NO recibe status (auto-inicializado a PENDING)
|
|
1503
|
+
public Order(String orderNumber, String customerId) {
|
|
1504
|
+
this.orderNumber = orderNumber;
|
|
1505
|
+
this.customerId = customerId;
|
|
1506
|
+
this.status = OrderStatus.PENDING; // ← initialValue
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Métodos de transición generados
|
|
1510
|
+
public void confirm() { this.status = this.status.transitionTo(OrderStatus.CONFIRMED); }
|
|
1511
|
+
public void ship() { this.status = this.status.transitionTo(OrderStatus.SHIPPED); }
|
|
1512
|
+
public void deliver() { this.status = this.status.transitionTo(OrderStatus.DELIVERED); }
|
|
1513
|
+
public void cancel() {
|
|
1514
|
+
if (this.status == OrderStatus.DELIVERED) {
|
|
1515
|
+
throw new BusinessException("Cannot execute 'cancel': business rule violated");
|
|
1516
|
+
}
|
|
1517
|
+
this.status = this.status.transitionTo(OrderStatus.CANCELLED);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Helpers de consulta
|
|
1521
|
+
public boolean isPending() { return this.status == OrderStatus.PENDING; }
|
|
1522
|
+
public boolean canConfirm() { return this.status.canTransitionTo(OrderStatus.CONFIRMED); }
|
|
1523
|
+
// ... uno por cada valor / transición
|
|
1524
|
+
```
|
|
1525
|
+
|
|
1526
|
+
### Domain Events (`events[]`)
|
|
1527
|
+
|
|
1528
|
+
```yaml
|
|
1529
|
+
aggregates:
|
|
1530
|
+
- name: Order
|
|
1531
|
+
entities:
|
|
1532
|
+
- name: order
|
|
1533
|
+
isRoot: true
|
|
1534
|
+
# ...
|
|
1535
|
+
events:
|
|
1536
|
+
- name: OrderConfirmedEvent
|
|
1537
|
+
fields:
|
|
1538
|
+
- name: orderId
|
|
1539
|
+
type: String
|
|
1540
|
+
- name: confirmedAt
|
|
1541
|
+
type: LocalDateTime
|
|
1542
|
+
# kafka: true # Optional — generates messageBroker.sendOrderConfirmedEvent()
|
|
1543
|
+
|
|
1544
|
+
- name: OrderShippedEvent
|
|
1545
|
+
kafka: true # Publishes to Kafka after commit
|
|
1546
|
+
fields:
|
|
1547
|
+
- name: orderId
|
|
1548
|
+
type: String
|
|
1549
|
+
- name: trackingNumber
|
|
1550
|
+
type: String
|
|
1551
|
+
```
|
|
1552
|
+
|
|
1553
|
+
**Archivos generados:**
|
|
1554
|
+
|
|
1555
|
+
`OrderConfirmedEvent.java` — en `domain/models/events/`:
|
|
1556
|
+
```java
|
|
1557
|
+
public final class OrderConfirmedEvent extends DomainEvent {
|
|
1558
|
+
private final String orderId;
|
|
1559
|
+
private final LocalDateTime confirmedAt;
|
|
1560
|
+
|
|
1561
|
+
public OrderConfirmedEvent(String aggregateId, String orderId, LocalDateTime confirmedAt) {
|
|
1562
|
+
super(aggregateId);
|
|
1563
|
+
this.orderId = orderId;
|
|
1564
|
+
this.confirmedAt = confirmedAt;
|
|
1565
|
+
}
|
|
1566
|
+
// getters
|
|
1567
|
+
}
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
`OrderDomainEventHandler.java` — en `application/usecases/`:
|
|
1571
|
+
```java
|
|
1572
|
+
@Component
|
|
1573
|
+
public class OrderDomainEventHandler {
|
|
1574
|
+
|
|
1575
|
+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
1576
|
+
public void handle(OrderConfirmedEvent event) { /* lógica post-commit */ }
|
|
1577
|
+
|
|
1578
|
+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
1579
|
+
public void handle(OrderShippedEvent event) {
|
|
1580
|
+
messageBroker.sendOrderShippedEvent(event); // kafka: true
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
```
|
|
1584
|
+
|
|
1585
|
+
**Publicar desde la entidad raíz** usando `raise()` heredado:
|
|
1586
|
+
```java
|
|
1587
|
+
public void confirm() {
|
|
1588
|
+
this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
|
|
1589
|
+
raise(new OrderConfirmedEvent(this.id, this.id, LocalDateTime.now()));
|
|
1590
|
+
}
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1164
1593
|
### Estrategias de Cascade
|
|
1165
1594
|
|
|
1166
1595
|
```yaml
|
|
@@ -1240,6 +1669,181 @@ HTTP Response (sin createdBy/updatedBy)
|
|
|
1240
1669
|
|
|
1241
1670
|
---
|
|
1242
1671
|
|
|
1672
|
+
## Soft Delete
|
|
1673
|
+
|
|
1674
|
+
Cuando una entidad tiene `hasSoftDelete: true` en `domain.yaml`, eva4j genera eliminación lógica en lugar de física.
|
|
1675
|
+
|
|
1676
|
+
### Configuración en domain.yaml
|
|
1677
|
+
|
|
1678
|
+
```yaml
|
|
1679
|
+
entities:
|
|
1680
|
+
- name: product
|
|
1681
|
+
isRoot: true
|
|
1682
|
+
tableName: products
|
|
1683
|
+
hasSoftDelete: true # ✅ Activa soft delete
|
|
1684
|
+
audit:
|
|
1685
|
+
enabled: true
|
|
1686
|
+
fields:
|
|
1687
|
+
- name: id
|
|
1688
|
+
type: String
|
|
1689
|
+
- name: name
|
|
1690
|
+
type: String
|
|
1691
|
+
```
|
|
1692
|
+
|
|
1693
|
+
### Comportamiento generado
|
|
1694
|
+
|
|
1695
|
+
```java
|
|
1696
|
+
// Entidad JPA — anotada con @SQLRestriction para filtrado automático
|
|
1697
|
+
@Entity
|
|
1698
|
+
@SQLRestriction("deleted_at IS NULL")
|
|
1699
|
+
public class ProductJpa extends AuditableEntity {
|
|
1700
|
+
@Id
|
|
1701
|
+
private String id;
|
|
1702
|
+
|
|
1703
|
+
@Column(name = "deleted_at")
|
|
1704
|
+
private LocalDateTime deletedAt; // null = activo, fecha = eliminado
|
|
1705
|
+
}
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
```java
|
|
1709
|
+
// Entidad de dominio — método de negocio para eliminar
|
|
1710
|
+
public class Product {
|
|
1711
|
+
private LocalDateTime deletedAt;
|
|
1712
|
+
|
|
1713
|
+
public void softDelete() {
|
|
1714
|
+
if (this.deletedAt != null) {
|
|
1715
|
+
throw new IllegalStateException("Product is already deleted");
|
|
1716
|
+
}
|
|
1717
|
+
this.deletedAt = LocalDateTime.now();
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
public boolean isDeleted() {
|
|
1721
|
+
return this.deletedAt != null;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
```
|
|
1725
|
+
|
|
1726
|
+
### Reglas para Agentes con Soft Delete
|
|
1727
|
+
|
|
1728
|
+
- **NUNCA** usar `repository.deleteById()` — usar `entity.softDelete()` + `repository.save(entity)`
|
|
1729
|
+
- **NUNCA** exponer `deletedAt` en ResponseDtos (campo interno)
|
|
1730
|
+
- **SIEMPRE** usar `@SQLRestriction("deleted_at IS NULL")` para filtrado automático en JPA
|
|
1731
|
+
- Los repositorios personalizados deben incluir el filtro si usan queries nativas
|
|
1732
|
+
|
|
1733
|
+
---
|
|
1734
|
+
|
|
1735
|
+
## Temporal Workflows
|
|
1736
|
+
|
|
1737
|
+
Cuando se agrega soporte de Temporal con `eva add temporal-client`, se genera infraestructura para workflows duraderos.
|
|
1738
|
+
|
|
1739
|
+
### Generación de archivos
|
|
1740
|
+
|
|
1741
|
+
```bash
|
|
1742
|
+
# 1. Agregar dependencias de Temporal al proyecto
|
|
1743
|
+
eva add temporal-client
|
|
1744
|
+
|
|
1745
|
+
# 2. Generar workflow para un módulo
|
|
1746
|
+
eva g temporal-flow order
|
|
1747
|
+
|
|
1748
|
+
# 3. Generar actividad para un módulo
|
|
1749
|
+
eva g temporal-activity order
|
|
1750
|
+
```
|
|
1751
|
+
|
|
1752
|
+
### Archivos generados
|
|
1753
|
+
|
|
1754
|
+
```
|
|
1755
|
+
[module]/
|
|
1756
|
+
├── application/
|
|
1757
|
+
│ └── workflows/
|
|
1758
|
+
│ ├── OrderWorkflow.java # Interface del workflow (@WorkflowInterface)
|
|
1759
|
+
│ └── OrderWorkflowImpl.java # Implementación
|
|
1760
|
+
└── infrastructure/
|
|
1761
|
+
└── temporal/
|
|
1762
|
+
├── activities/
|
|
1763
|
+
│ ├── OrderActivity.java # Interface de actividad (@ActivityInterface)
|
|
1764
|
+
│ └── OrderActivityImpl.java # Implementación de actividad
|
|
1765
|
+
└── workers/
|
|
1766
|
+
└── OrderWorker.java # Registro del worker en Spring
|
|
1767
|
+
```
|
|
1768
|
+
|
|
1769
|
+
### Patrón de uso
|
|
1770
|
+
|
|
1771
|
+
**Workflow Interface**:
|
|
1772
|
+
```java
|
|
1773
|
+
@WorkflowInterface
|
|
1774
|
+
public interface OrderWorkflow {
|
|
1775
|
+
@WorkflowMethod
|
|
1776
|
+
String processOrder(String orderId);
|
|
1777
|
+
}
|
|
1778
|
+
```
|
|
1779
|
+
|
|
1780
|
+
**Activity Interface**:
|
|
1781
|
+
```java
|
|
1782
|
+
@ActivityInterface
|
|
1783
|
+
public interface OrderActivity {
|
|
1784
|
+
@ActivityMethod
|
|
1785
|
+
void validateOrder(String orderId);
|
|
1786
|
+
|
|
1787
|
+
@ActivityMethod
|
|
1788
|
+
void chargePayment(String orderId);
|
|
1789
|
+
|
|
1790
|
+
@ActivityMethod
|
|
1791
|
+
void fulfillOrder(String orderId);
|
|
1792
|
+
}
|
|
1793
|
+
```
|
|
1794
|
+
|
|
1795
|
+
**Workflow Implementation** (determinista — sin I/O directo):
|
|
1796
|
+
```java
|
|
1797
|
+
public class OrderWorkflowImpl implements OrderWorkflow {
|
|
1798
|
+
private final OrderActivity activity = Workflow.newActivityStub(
|
|
1799
|
+
OrderActivity.class,
|
|
1800
|
+
ActivityOptions.newBuilder()
|
|
1801
|
+
.setStartToCloseTimeout(Duration.ofSeconds(30))
|
|
1802
|
+
.build()
|
|
1803
|
+
);
|
|
1804
|
+
|
|
1805
|
+
@Override
|
|
1806
|
+
public String processOrder(String orderId) {
|
|
1807
|
+
activity.validateOrder(orderId);
|
|
1808
|
+
activity.chargePayment(orderId);
|
|
1809
|
+
activity.fulfillOrder(orderId);
|
|
1810
|
+
return "COMPLETED";
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
```
|
|
1814
|
+
|
|
1815
|
+
**Invocar desde un Use Case**:
|
|
1816
|
+
```java
|
|
1817
|
+
@Service
|
|
1818
|
+
public class StartOrderProcessingCommandHandler
|
|
1819
|
+
implements CommandHandler<StartOrderProcessingCommand, String> {
|
|
1820
|
+
|
|
1821
|
+
private final WorkflowClient workflowClient;
|
|
1822
|
+
|
|
1823
|
+
@Override
|
|
1824
|
+
public String handle(StartOrderProcessingCommand command) {
|
|
1825
|
+
OrderWorkflow workflow = workflowClient.newWorkflowStub(
|
|
1826
|
+
OrderWorkflow.class,
|
|
1827
|
+
WorkflowOptions.newBuilder()
|
|
1828
|
+
.setWorkflowId("order-" + command.orderId())
|
|
1829
|
+
.setTaskQueue("order-task-queue")
|
|
1830
|
+
.build()
|
|
1831
|
+
);
|
|
1832
|
+
return WorkflowClient.start(workflow::processOrder, command.orderId());
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
```
|
|
1836
|
+
|
|
1837
|
+
### Reglas para Agentes con Temporal
|
|
1838
|
+
|
|
1839
|
+
- Los workflows deben ser **deterministas** — sin `Math.random()`, sin `new Date()`, sin I/O
|
|
1840
|
+
- Toda operación con efectos secundarios (DB, HTTP, emails) va en **Activities**, no en Workflows
|
|
1841
|
+
- El **Worker** debe registrar tanto el workflow como las activities al arrancar
|
|
1842
|
+
- Configuración de conexión en `resources/parameters/{env}/temporal.yaml`
|
|
1843
|
+
- Los Use Cases **orquestan** workflows; los Activities **ejecutan** lógica de infraestructura
|
|
1844
|
+
|
|
1845
|
+
---
|
|
1846
|
+
|
|
1243
1847
|
## Configuration Management
|
|
1244
1848
|
|
|
1245
1849
|
### Environment-Specific Configuration
|
|
@@ -1385,6 +1989,15 @@ public record UserResponseDto(
|
|
|
1385
1989
|
|
|
1386
1990
|
## 🎯 Mejores Prácticas para Agentes
|
|
1387
1991
|
|
|
1992
|
+
### Al Generar domain.yaml (Flujo SDD)
|
|
1993
|
+
|
|
1994
|
+
1. **SIEMPRE** incluir campo `id` en todas las entidades
|
|
1995
|
+
2. **SI** el módulo requiere ciclo de vida → usar `transitions` + `initialValue` en el enum
|
|
1996
|
+
3. **SI** un Value Object tiene comportamiento → declarar `methods` en `domain.yaml`
|
|
1997
|
+
4. **SI** ocurren hechos relevantes de negocio → declarar `events[]` en el agregado
|
|
1998
|
+
5. **SI** el evento debe propagarse a otros servicios → agregar `kafka: true` al evento
|
|
1999
|
+
6. **DESPUÉS** de generar el `domain.yaml` → ejecutar `eva g entities <module>`
|
|
2000
|
+
|
|
1388
2001
|
### Al Generar Código de Dominio
|
|
1389
2002
|
|
|
1390
2003
|
1. **NUNCA** crear constructor vacío en entidades de dominio
|
|
@@ -1392,6 +2005,7 @@ public record UserResponseDto(
|
|
|
1392
2005
|
3. **SIEMPRE** crear métodos de negocio para modificar estado
|
|
1393
2006
|
4. **SIEMPRE** validar en métodos de negocio, no en constructores
|
|
1394
2007
|
5. **SIEMPRE** mantener inmutabilidad en Value Objects
|
|
2008
|
+
6. **NUNCA** agregar anotaciones JSR-303 (`@NotNull`, `@Size`) en entidades de dominio
|
|
1395
2009
|
|
|
1396
2010
|
### Al Generar Código JPA
|
|
1397
2011
|
|
|
@@ -1402,19 +2016,30 @@ public record UserResponseDto(
|
|
|
1402
2016
|
- `audit.trackUser: true`: extender `FullAuditableEntity`
|
|
1403
2017
|
3. **NUNCA** incluir campos de auditoría heredados en `@Builder`
|
|
1404
2018
|
4. **SIEMPRE** usar `@NoArgsConstructor` para JPA
|
|
2019
|
+
5. **SIEMPRE** agregar `@SQLRestriction("deleted_at IS NULL")` cuando `hasSoftDelete: true`
|
|
1405
2020
|
|
|
1406
2021
|
### Al Generar Mappers
|
|
1407
2022
|
|
|
1408
|
-
1. **NUNCA** mapear campos de auditoría (createdAt
|
|
1409
|
-
2. **
|
|
1410
|
-
3. **
|
|
2023
|
+
1. **NUNCA** mapear campos de auditoría (`createdAt`, `updatedAt`, `createdBy`, `updatedBy`)
|
|
2024
|
+
2. **NUNCA** mapear campos `readOnly` en métodos de creación (`fromCommand`, `fromDto`)
|
|
2025
|
+
3. **NUNCA** mapear campos `hidden` en métodos de respuesta (`toDto`, `toResponseDto`)
|
|
2026
|
+
4. **SIEMPRE** filtrar campos antes de usar `.builder()`
|
|
2027
|
+
5. **SIEMPRE** mapear bidireccionalidad en relaciones
|
|
1411
2028
|
|
|
1412
2029
|
### Al Generar DTOs
|
|
1413
2030
|
|
|
1414
2031
|
1. **NUNCA** exponer `createdBy` y `updatedBy` en respuestas
|
|
1415
|
-
2. **
|
|
1416
|
-
3. **
|
|
1417
|
-
4. **SIEMPRE**
|
|
2032
|
+
2. **NUNCA** incluir campos `readOnly` en CreateDto
|
|
2033
|
+
3. **NUNCA** incluir campos `hidden` en ResponseDto
|
|
2034
|
+
4. **SIEMPRE** exponer `createdAt` y `updatedAt` cuando hay auditoría
|
|
2035
|
+
5. **SIEMPRE** usar Java Records para DTOs
|
|
2036
|
+
|
|
2037
|
+
### Al Agregar Validaciones
|
|
2038
|
+
|
|
2039
|
+
1. **SIEMPRE** agregar validaciones JSR-303 **solo** en Commands y CreateDtos
|
|
2040
|
+
2. **NUNCA** agregar `@Valid` en entidades de dominio
|
|
2041
|
+
3. **SIEMPRE** agregar `@Valid` en el parámetro del endpoint REST que recibe el DTO
|
|
2042
|
+
4. Los mensajes de error de validación los maneja el `@ControllerAdvice` global generado en `common/`
|
|
1418
2043
|
|
|
1419
2044
|
---
|
|
1420
2045
|
|
|
@@ -1422,15 +2047,49 @@ public record UserResponseDto(
|
|
|
1422
2047
|
|
|
1423
2048
|
Al generar o modificar código, verificar:
|
|
1424
2049
|
|
|
1425
|
-
|
|
1426
|
-
- [ ]
|
|
1427
|
-
- [ ]
|
|
1428
|
-
- [ ]
|
|
1429
|
-
- [ ]
|
|
1430
|
-
- [ ]
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
- [ ]
|
|
2050
|
+
**Entidades de Dominio:**
|
|
2051
|
+
- [ ] Sin constructor vacío
|
|
2052
|
+
- [ ] Sin setters públicos
|
|
2053
|
+
- [ ] Métodos de negocio con validaciones explícitas
|
|
2054
|
+
- [ ] Value Objects inmutables
|
|
2055
|
+
- [ ] Sin anotaciones JSR-303 en dominio
|
|
2056
|
+
|
|
2057
|
+
**Entidades JPA:**
|
|
2058
|
+
- [ ] Usa Lombok (`@Getter`, `@Setter`, `@Builder`, `@NoArgsConstructor`)
|
|
2059
|
+
- [ ] Extiende la clase base correcta según auditoría (`AuditableEntity` / `FullAuditableEntity`)
|
|
2060
|
+
- [ ] `@SQLRestriction("deleted_at IS NULL")` cuando `hasSoftDelete: true`
|
|
2061
|
+
- [ ] No incluye campos de auditoría heredados en `@Builder`
|
|
2062
|
+
|
|
2063
|
+
**Mappers:**
|
|
2064
|
+
- [ ] Excluyen campos de auditoría (`createdAt`, `updatedAt`, `createdBy`, `updatedBy`)
|
|
2065
|
+
- [ ] Excluyen campos `readOnly` en métodos de creación
|
|
2066
|
+
- [ ] Excluyen campos `hidden` en métodos de respuesta
|
|
2067
|
+
- [ ] Mapean bidireccionalidad en relaciones con `assign*()`
|
|
2068
|
+
|
|
2069
|
+
**DTOs:**
|
|
2070
|
+
- [ ] ResponseDto sin `createdBy`/`updatedBy`
|
|
2071
|
+
- [ ] ResponseDto sin campos `hidden`
|
|
2072
|
+
- [ ] CreateDto sin campos `readOnly`
|
|
2073
|
+
- [ ] Usan Java Records
|
|
2074
|
+
|
|
2075
|
+
**Validaciones:**
|
|
2076
|
+
- [ ] JSR-303 solo en Command y CreateDto, nunca en dominio
|
|
2077
|
+
- [ ] `@Valid` presente en parámetros de endpoints REST
|
|
2078
|
+
|
|
2079
|
+
**Auditoría:**
|
|
2080
|
+
- [ ] Configurada correctamente cuando `audit.trackUser: true`
|
|
2081
|
+
- [ ] `UserContextFilter` y `UserContextHolder` presentes en `common/`
|
|
2082
|
+
- [ ] `@EnableJpaAuditing` con `auditorAwareRef = "auditorProvider"` en Application.java
|
|
2083
|
+
|
|
2084
|
+
**Soft Delete (cuando aplica):**
|
|
2085
|
+
- [ ] Usar `entity.softDelete()` + `repository.save()` — nunca `deleteById()`
|
|
2086
|
+
- [ ] `deletedAt` no expuesto en ResponseDto
|
|
2087
|
+
|
|
2088
|
+
**Características Avanzadas (cuando aplica):**
|
|
2089
|
+
- [ ] Enum con ciclo de vida → `transitions` + `initialValue`, no setters
|
|
2090
|
+
- [ ] Value Object con comportamiento → `methods` en domain.yaml
|
|
2091
|
+
- [ ] Evento de dominio → `events[]`, publicar con `raise()` en método de negocio
|
|
2092
|
+
- [ ] Evento con Kafka → `kafka: true` en el evento
|
|
1434
2093
|
|
|
1435
2094
|
---
|
|
1436
2095
|
|
|
@@ -1440,7 +2099,7 @@ Al generar o modificar código, verificar:
|
|
|
1440
2099
|
|
|
1441
2100
|
1. **domain.yaml is the source of truth** for domain models
|
|
1442
2101
|
2. **Never edit generated files** directly (domain/models/entities, infrastructure/database/entities)
|
|
1443
|
-
3. **Always regenerate** after modifying domain.yaml: `
|
|
2102
|
+
3. **Always regenerate** after modifying domain.yaml: `eva g entities <module>`
|
|
1444
2103
|
4. **Hand-written code** goes in:
|
|
1445
2104
|
- Application layer (use cases, commands, queries)
|
|
1446
2105
|
- Domain services
|
|
@@ -1463,6 +2122,205 @@ Al generar o modificar código, verificar:
|
|
|
1463
2122
|
- ❌ Repository implementations (infrastructure/database/repositories)
|
|
1464
2123
|
- ❌ Mappers (infrastructure/database/mappers)
|
|
1465
2124
|
|
|
2125
|
+
---
|
|
2126
|
+
|
|
2127
|
+
## 🧪 Testing
|
|
2128
|
+
|
|
2129
|
+
### Tests de Dominio (Unidad)
|
|
2130
|
+
|
|
2131
|
+
Las entidades de dominio se testean sin ningún framework — solo Java puro:
|
|
2132
|
+
|
|
2133
|
+
```java
|
|
2134
|
+
class OrderTest {
|
|
2135
|
+
|
|
2136
|
+
@Test
|
|
2137
|
+
void shouldCreateOrderWithValidData() {
|
|
2138
|
+
Order order = new Order("customer-123", LocalDateTime.now());
|
|
2139
|
+
|
|
2140
|
+
assertEquals("customer-123", order.getCustomerId());
|
|
2141
|
+
assertNotNull(order.getOrderDate());
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
@Test
|
|
2145
|
+
void shouldConfirmOrder() {
|
|
2146
|
+
Order order = new Order("customer-123", LocalDateTime.now());
|
|
2147
|
+
|
|
2148
|
+
order.confirm();
|
|
2149
|
+
|
|
2150
|
+
assertEquals(OrderStatus.CONFIRMED, order.getStatus());
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
@Test
|
|
2154
|
+
void shouldNotConfirmCancelledOrder() {
|
|
2155
|
+
Order order = new Order("customer-123", LocalDateTime.now());
|
|
2156
|
+
order.cancel();
|
|
2157
|
+
|
|
2158
|
+
assertThrows(IllegalStateException.class, order::confirm);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
```
|
|
2162
|
+
|
|
2163
|
+
### Object Mother Pattern (Test Data Builders)
|
|
2164
|
+
|
|
2165
|
+
Crear clases `Mother` para construir objetos de prueba reutilizables:
|
|
2166
|
+
|
|
2167
|
+
```java
|
|
2168
|
+
// src/test/java/[package]/order/domain/OrderMother.java
|
|
2169
|
+
public class OrderMother {
|
|
2170
|
+
|
|
2171
|
+
public static Order draft() {
|
|
2172
|
+
return new Order("customer-123", LocalDateTime.now());
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
public static Order confirmed() {
|
|
2176
|
+
Order order = draft();
|
|
2177
|
+
order.confirm();
|
|
2178
|
+
return order;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
public static Order cancelled() {
|
|
2182
|
+
Order order = draft();
|
|
2183
|
+
order.cancel();
|
|
2184
|
+
return order;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
public static Order withCustomer(String customerId) {
|
|
2188
|
+
return new Order(customerId, LocalDateTime.now());
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
```
|
|
2192
|
+
|
|
2193
|
+
**Uso en tests**:
|
|
2194
|
+
```java
|
|
2195
|
+
@Test
|
|
2196
|
+
void shouldNotCancelDeliveredOrder() {
|
|
2197
|
+
Order delivered = OrderMother.withStatus(OrderStatus.DELIVERED);
|
|
2198
|
+
|
|
2199
|
+
assertThrows(IllegalStateException.class, () -> delivered.cancel());
|
|
2200
|
+
}
|
|
2201
|
+
```
|
|
2202
|
+
|
|
2203
|
+
### Repositorio Fake (In-Memory)
|
|
2204
|
+
|
|
2205
|
+
Para testear Use Cases sin base de datos, usar implementaciones in-memory:
|
|
2206
|
+
|
|
2207
|
+
```java
|
|
2208
|
+
// src/test/java/[package]/order/infrastructure/OrderRepositoryFake.java
|
|
2209
|
+
public class OrderRepositoryFake implements OrderRepository {
|
|
2210
|
+
private final Map<String, Order> store = new HashMap<>();
|
|
2211
|
+
|
|
2212
|
+
@Override
|
|
2213
|
+
public Order save(Order order) {
|
|
2214
|
+
store.put(order.getId(), order);
|
|
2215
|
+
return order;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
@Override
|
|
2219
|
+
public Optional<Order> findById(String id) {
|
|
2220
|
+
return Optional.ofNullable(store.get(id));
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
@Override
|
|
2224
|
+
public List<Order> findAll() {
|
|
2225
|
+
return new ArrayList<>(store.values());
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
@Override
|
|
2229
|
+
public void deleteById(String id) {
|
|
2230
|
+
store.remove(id);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// Helper para assertions en tests
|
|
2234
|
+
public int count() {
|
|
2235
|
+
return store.size();
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
public boolean contains(String id) {
|
|
2239
|
+
return store.containsKey(id);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
```
|
|
2243
|
+
|
|
2244
|
+
### Tests de Use Cases
|
|
2245
|
+
|
|
2246
|
+
```java
|
|
2247
|
+
class CreateOrderCommandHandlerTest {
|
|
2248
|
+
|
|
2249
|
+
private final OrderRepositoryFake orderRepository = new OrderRepositoryFake();
|
|
2250
|
+
private final CreateOrderCommandHandler handler =
|
|
2251
|
+
new CreateOrderCommandHandler(orderRepository);
|
|
2252
|
+
|
|
2253
|
+
@Test
|
|
2254
|
+
void shouldCreateOrder() {
|
|
2255
|
+
CreateOrderCommand command = new CreateOrderCommand("customer-123");
|
|
2256
|
+
|
|
2257
|
+
String orderId = handler.handle(command);
|
|
2258
|
+
|
|
2259
|
+
assertNotNull(orderId);
|
|
2260
|
+
assertEquals(1, orderRepository.count());
|
|
2261
|
+
assertTrue(orderRepository.contains(orderId));
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
```
|
|
2265
|
+
|
|
2266
|
+
### Tests de Integración (con Testcontainers)
|
|
2267
|
+
|
|
2268
|
+
Para tests de integración con base de datos real:
|
|
2269
|
+
|
|
2270
|
+
```java
|
|
2271
|
+
@SpringBootTest
|
|
2272
|
+
@Testcontainers
|
|
2273
|
+
class OrderRepositoryImplTest {
|
|
2274
|
+
|
|
2275
|
+
@Container
|
|
2276
|
+
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
|
|
2277
|
+
.withDatabaseName("test_db")
|
|
2278
|
+
.withUsername("test")
|
|
2279
|
+
.withPassword("test");
|
|
2280
|
+
|
|
2281
|
+
@DynamicPropertySource
|
|
2282
|
+
static void overrideProperties(DynamicPropertyRegistry registry) {
|
|
2283
|
+
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
2284
|
+
registry.add("spring.datasource.username", postgres::getUsername);
|
|
2285
|
+
registry.add("spring.datasource.password", postgres::getPassword);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
@Autowired
|
|
2289
|
+
private OrderRepository orderRepository;
|
|
2290
|
+
|
|
2291
|
+
@Test
|
|
2292
|
+
void shouldPersistAndRetrieveOrder() {
|
|
2293
|
+
Order order = OrderMother.draft();
|
|
2294
|
+
Order saved = orderRepository.save(order);
|
|
2295
|
+
|
|
2296
|
+
Optional<Order> found = orderRepository.findById(saved.getId());
|
|
2297
|
+
|
|
2298
|
+
assertTrue(found.isPresent());
|
|
2299
|
+
assertEquals(saved.getId(), found.get().getId());
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
```
|
|
2303
|
+
|
|
2304
|
+
### Estrategia de Testing por Capa
|
|
2305
|
+
|
|
2306
|
+
| Capa | Tipo de Test | Framework |
|
|
2307
|
+
|------|-------------|-----------|
|
|
2308
|
+
| Domain entities | Unidad pura | JUnit 5 |
|
|
2309
|
+
| Use cases (Command/Query handlers) | Unidad con Fakes | JUnit 5 + Fake repos |
|
|
2310
|
+
| Application mappers | Unidad | JUnit 5 |
|
|
2311
|
+
| Repository implementations | Integración | Testcontainers |
|
|
2312
|
+
| REST controllers | Integración | @WebMvcTest / MockMvc |
|
|
2313
|
+
|
|
2314
|
+
### Reglas para Agentes al generar Tests
|
|
2315
|
+
|
|
2316
|
+
- **SIEMPRE** usar Object Mother pattern para datos de prueba
|
|
2317
|
+
- **SIEMPRE** usar Fake repositories en tests de use cases (no mocks)
|
|
2318
|
+
- **NUNCA** poner lógica de negocio en los tests — solo verificar comportamiento
|
|
2319
|
+
- **NUNCA** testear getters/setters — testear comportamiento de negocio
|
|
2320
|
+
- Validaciones JSR-303 se verifican en tests de controladores con `@Valid`, no en dominio
|
|
2321
|
+
|
|
2322
|
+
---
|
|
2323
|
+
|
|
1466
2324
|
### Documentation References
|
|
1467
2325
|
|
|
1468
2326
|
- **[DOMAIN_YAML_GUIDE.md](https://github.com/asuridev/eva4j)**: Complete domain.yaml syntax reference
|
|
@@ -1475,29 +2333,36 @@ Al generar o modificar código, verificar:
|
|
|
1475
2333
|
|
|
1476
2334
|
```bash
|
|
1477
2335
|
# Project management
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
2336
|
+
eva create <project> # Create new project
|
|
2337
|
+
eva add module <name> # Add module
|
|
2338
|
+
eva info # View config
|
|
1481
2339
|
|
|
1482
2340
|
# Code generation
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
2341
|
+
eva g entities <module> # Generate from domain.yaml
|
|
2342
|
+
eva g resource <module> # Generate CRUD REST
|
|
2343
|
+
eva g usecase <module> <name> # Add custom use case
|
|
2344
|
+
eva g record # Generate record from JSON
|
|
1487
2345
|
|
|
1488
2346
|
# Event-driven
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
2347
|
+
eva add kafka-client # Enable Kafka
|
|
2348
|
+
eva g kafka-event <mod> <event> # Publish events
|
|
2349
|
+
eva g kafka-listener <module> # Consume events
|
|
1492
2350
|
|
|
1493
2351
|
# External integration
|
|
1494
|
-
|
|
2352
|
+
eva g http-exchange <mod> <api> # External API client
|
|
2353
|
+
|
|
2354
|
+
# Temporal workflows
|
|
2355
|
+
eva add temporal-client # Enable Temporal
|
|
2356
|
+
eva g temporal-flow <module> # Generate workflow definition
|
|
2357
|
+
eva g temporal-activity <module> # Generate activity
|
|
1495
2358
|
|
|
1496
2359
|
# Microservices
|
|
1497
|
-
|
|
2360
|
+
eva detach <module> # Extract to microservice
|
|
1498
2361
|
```
|
|
1499
2362
|
|
|
1500
2363
|
---
|
|
1501
2364
|
|
|
1502
|
-
**
|
|
1503
|
-
|
|
2365
|
+
**Generado por eva4j CLI** — Versión `<%= version || '1.0.0' %>`
|
|
2366
|
+
**Proyecto**: `<%= artifactId %>` | **Paquete base**: `<%= packageName %>`
|
|
2367
|
+
**Fecha de generación**: `<%= createdDate %>`
|
|
2368
|
+
Para más información: https://github.com/asuridev/eva4j
|