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.
@@ -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 `eva4j add module`)
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 `eva4j detach`
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 `eva4j g resource`)
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
- eva4j create my-project
813
+ eva create my-project
693
814
 
694
815
  # Add a new module
695
- eva4j add module order
816
+ eva add module order
696
817
 
697
818
  # View project configuration
698
- eva4j info
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
- eva4j generate entities order
705
- eva4j g entities order # Alias
825
+ eva generate entities order
826
+ eva g entities order # Alias
706
827
 
707
828
  # Generate CRUD REST API
708
- eva4j g resource order # Creates controller + DTOs + CRUD handlers
829
+ eva g resource order # Creates controller + DTOs + CRUD handlers
709
830
 
710
831
  # Generate custom use case
711
- eva4j g usecase order CreateCustomOrder
832
+ eva g usecase order CreateCustomOrder
712
833
  ```
713
834
 
714
835
  ### Event-Driven Features
715
836
  ```bash
716
837
  # Add Kafka client
717
- eva4j add kafka-client
838
+ eva add kafka-client
718
839
 
719
840
  # Publish events
720
- eva4j g kafka-event order order-created
841
+ eva g kafka-event order order-created
721
842
 
722
843
  # Consume events
723
- eva4j g kafka-listener notification
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
- eva4j g http-exchange order payment-service
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
- eva4j g record
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
- eva4j detach order # Creates ../order_msvc/ project
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
- eva4j g entities order
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
- eva4j g entities order
1131
+ eva g entities order
954
1132
  ```
955
1133
 
956
1134
  ### 3. Adding a Custom Use Case
957
1135
 
958
1136
  ```bash
959
- eva4j g usecase order CancelOrder
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
- eva4j g kafka-event order order-cancelled
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
- eva4j g kafka-listener notification
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, updatedAt, createdBy, updatedBy)
1409
- 2. **SIEMPRE** filtrar campos antes de usar `.builder()`
1410
- 3. **SIEMPRE** mapear bidireccionalidad en relaciones
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. **SIEMPRE** exponer `createdAt` y `updatedAt`
1416
- 3. **SIEMPRE** usar Java Records para DTOs
1417
- 4. **SIEMPRE** filtrar campos al crear contextos de template
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
- - [ ] Entidades de dominio **sin constructor vacío**
1426
- - [ ] Entidades de dominio **sin setters públicos**
1427
- - [ ] Métodos de negocio con **validaciones explícitas**
1428
- - [ ] Entidades JPA con **Lombok y herencia correcta**
1429
- - [ ] Mappers **excluyen campos de auditoría**
1430
- - [ ] DTOs de respuesta **sin createdBy/updatedBy**
1431
- - [ ] Relaciones bidireccionales con métodos `assign*()`
1432
- - [ ] Value Objects **inmutables**
1433
- - [ ] Configuración de auditoría cuando `audit.trackUser: true`
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: `eva4j g entities <module>`
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
- eva4j create <project> # Create new project
1479
- eva4j add module <name> # Add module
1480
- eva4j info # View config
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
- eva4j g entities <module> # Generate from domain.yaml
1484
- eva4j g resource <module> # Generate CRUD REST
1485
- eva4j g usecase <module> <name> # Add custom use case
1486
- eva4j g record # Generate record from JSON
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
- eva4j add kafka-client # Enable Kafka
1490
- eva4j g kafka-event <mod> <event> # Publish events
1491
- eva4j g kafka-listener <module> # Consume events
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
- eva4j g http-exchange <mod> <api> # External API client
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
- eva4j detach <module> # Extract to microservice
2360
+ eva detach <module> # Extract to microservice
1498
2361
  ```
1499
2362
 
1500
2363
  ---
1501
2364
 
1502
- **Generated by eva4j CLI** - Version <%= version || '1.0.0' %>
1503
- For more information, visit: https://github.com/asuridev/eva4j
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