eva4j 1.0.16 → 1.0.18
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 +220 -5
- package/DOMAIN_YAML_GUIDE.md +188 -3
- package/FUTURE_FEATURES.md +33 -52
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +290 -10
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +34 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +560 -24
- package/src/commands/generate-http-exchange.js +3 -0
- package/src/commands/generate-kafka-event.js +3 -0
- package/src/commands/generate-kafka-listener.js +3 -0
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +970 -33
- package/src/commands/generate-temporal-flow.js +98 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +343 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
- package/src/skills/build-system-yaml/references/module-spec.md +90 -9
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +495 -17
- package/src/utils/naming.js +20 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +281 -9
- package/templates/aggregate/AggregateRepository.java.ejs +4 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
- package/templates/aggregate/AggregateRoot.java.ejs +38 -4
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelDomain.java.ejs +46 -0
- package/templates/read-model/ReadModelJpa.java.ejs +58 -0
- package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- package/COMMAND_EVALUATION.md +0 -911
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
# Use Case Implementation Patterns
|
|
2
|
+
|
|
3
|
+
Referencia completa de patrones de implementación para cada tipo de caso de uso en proyectos eva4j. Lee esta referencia cuando necesites ver código de ejemplo detallado para un patrón específico.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Tabla de contenido
|
|
8
|
+
|
|
9
|
+
1. [Query por ID (GetEntity)](#1-query-por-id)
|
|
10
|
+
2. [Query paginada (FindAll)](#2-query-paginada)
|
|
11
|
+
3. [Query con filtros custom](#3-query-con-filtros-custom)
|
|
12
|
+
4. [Query con filtros múltiples opcionales](#4-query-con-filtros-múltiples-opcionales)
|
|
13
|
+
5. [Command de creación con unicidad](#5-command-de-creación-con-unicidad)
|
|
14
|
+
6. [Command de actualización (PATCH merge)](#6-command-de-actualización)
|
|
15
|
+
7. [Command de transición de estado](#7-command-de-transición-de-estado)
|
|
16
|
+
8. [Command con soft delete](#8-command-con-soft-delete)
|
|
17
|
+
9. [Command que emite eventos](#9-command-que-emite-eventos)
|
|
18
|
+
10. [Activity de Temporal (light)](#10-activity-de-temporal-light)
|
|
19
|
+
11. [Activity de Temporal (heavy)](#11-activity-de-temporal-heavy)
|
|
20
|
+
12. [Command sobre entidad secundaria del agregado](#12-command-sobre-entidad-secundaria)
|
|
21
|
+
13. [Query con proyección parcial](#13-query-con-proyección-parcial)
|
|
22
|
+
14. [Agregar métodos al repositorio](#14-agregar-métodos-al-repositorio)
|
|
23
|
+
15. [Crear excepciones custom](#15-crear-excepciones-custom)
|
|
24
|
+
16. [Activity cross-module — leer el workflow como spec](#16-activity-cross-module)
|
|
25
|
+
17. [Activity de compensación (rollback)](#17-activity-de-compensación)
|
|
26
|
+
18. [Activity void — transición de estado sin retorno](#18-activity-void)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. Query por ID
|
|
31
|
+
|
|
32
|
+
**Caso:** `GetProduct`, `GetOrder`, `GetCustomer`
|
|
33
|
+
|
|
34
|
+
```java
|
|
35
|
+
@ApplicationComponent
|
|
36
|
+
public class GetProductQueryHandler
|
|
37
|
+
implements QueryHandler<GetProductQuery, ProductResponseDto> {
|
|
38
|
+
|
|
39
|
+
private final ProductRepository repository;
|
|
40
|
+
private final ProductApplicationMapper mapper;
|
|
41
|
+
|
|
42
|
+
public GetProductQueryHandler(ProductRepository repository,
|
|
43
|
+
ProductApplicationMapper mapper) {
|
|
44
|
+
this.repository = repository;
|
|
45
|
+
this.mapper = mapper;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Override
|
|
49
|
+
@Transactional(readOnly = true)
|
|
50
|
+
@LogExceptions
|
|
51
|
+
public ProductResponseDto handle(GetProductQuery query) {
|
|
52
|
+
Product entity = repository.findById(query.id())
|
|
53
|
+
.orElseThrow(() -> new NotFoundException("Product not found with id: " + query.id()));
|
|
54
|
+
|
|
55
|
+
return mapper.toDto(entity);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Puntos clave:**
|
|
61
|
+
- `@Transactional(readOnly = true)` — optimiza la conexión a BD
|
|
62
|
+
- `NotFoundException` → HTTP 404 (manejado por `HandlerExceptions`)
|
|
63
|
+
- Siempre devuelve DTO, nunca la entidad de dominio
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 2. Query paginada
|
|
68
|
+
|
|
69
|
+
**Caso:** `FindAllProducts`, `FindAllOrders`, `FindAllCustomers`
|
|
70
|
+
|
|
71
|
+
```java
|
|
72
|
+
@ApplicationComponent
|
|
73
|
+
public class FindAllProductsQueryHandler
|
|
74
|
+
implements QueryHandler<FindAllProductsQuery, PagedResponse<ProductResponseDto>> {
|
|
75
|
+
|
|
76
|
+
private final ProductRepository repository;
|
|
77
|
+
private final ProductApplicationMapper mapper;
|
|
78
|
+
|
|
79
|
+
public FindAllProductsQueryHandler(ProductRepository repository,
|
|
80
|
+
ProductApplicationMapper mapper) {
|
|
81
|
+
this.repository = repository;
|
|
82
|
+
this.mapper = mapper;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Override
|
|
86
|
+
@Transactional(readOnly = true)
|
|
87
|
+
@LogExceptions
|
|
88
|
+
public PagedResponse<ProductResponseDto> handle(FindAllProductsQuery query) {
|
|
89
|
+
Sort sort = Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy());
|
|
90
|
+
Pageable pageable = PageRequest.of(query.page(), query.size(), sort);
|
|
91
|
+
|
|
92
|
+
Page<Product> page = repository.findAll(pageable);
|
|
93
|
+
List<ProductResponseDto> content = page.getContent().stream()
|
|
94
|
+
.map(mapper::toDto)
|
|
95
|
+
.toList();
|
|
96
|
+
|
|
97
|
+
return PagedResponse.of(content, page.getNumber(), page.getSize(), page.getTotalElements());
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Imports necesarios:**
|
|
103
|
+
```java
|
|
104
|
+
import org.springframework.data.domain.Page;
|
|
105
|
+
import org.springframework.data.domain.PageRequest;
|
|
106
|
+
import org.springframework.data.domain.Pageable;
|
|
107
|
+
import org.springframework.data.domain.Sort;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 3. Query con filtros custom
|
|
113
|
+
|
|
114
|
+
**Caso:** `FindProductsByCategory`, `FindOrdersByCustomer`
|
|
115
|
+
|
|
116
|
+
Cuando la query filtra por un campo específico, necesitas agregar el método en 3 niveles del repositorio.
|
|
117
|
+
|
|
118
|
+
### Paso 1 — Repositorio de dominio
|
|
119
|
+
|
|
120
|
+
```java
|
|
121
|
+
// domain/repositories/ProductRepository.java
|
|
122
|
+
public interface ProductRepository {
|
|
123
|
+
// ... métodos existentes ...
|
|
124
|
+
Page<Product> findByCategoryId(String categoryId, Pageable pageable);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Paso 2 — JPA Repository
|
|
129
|
+
|
|
130
|
+
```java
|
|
131
|
+
// infrastructure/database/repositories/ProductJpaRepository.java
|
|
132
|
+
public interface ProductJpaRepository extends JpaRepository<ProductJpa, String> {
|
|
133
|
+
Page<ProductJpa> findByCategoryId(String categoryId, Pageable pageable);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Spring Data genera la query automáticamente por convención de nombres.
|
|
138
|
+
|
|
139
|
+
### Paso 3 — Repository Implementation
|
|
140
|
+
|
|
141
|
+
```java
|
|
142
|
+
// infrastructure/database/repositories/ProductRepositoryImpl.java
|
|
143
|
+
@Override
|
|
144
|
+
public Page<Product> findByCategoryId(String categoryId, Pageable pageable) {
|
|
145
|
+
return jpaRepository.findByCategoryId(categoryId, pageable).map(mapper::toDomain);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Paso 4 — Handler
|
|
150
|
+
|
|
151
|
+
```java
|
|
152
|
+
@ApplicationComponent
|
|
153
|
+
public class FindProductsByCategoryQueryHandler
|
|
154
|
+
implements QueryHandler<FindProductsByCategoryQuery, PagedResponse<ProductResponseDto>> {
|
|
155
|
+
|
|
156
|
+
private final ProductRepository repository;
|
|
157
|
+
private final ProductApplicationMapper mapper;
|
|
158
|
+
|
|
159
|
+
public FindProductsByCategoryQueryHandler(ProductRepository repository,
|
|
160
|
+
ProductApplicationMapper mapper) {
|
|
161
|
+
this.repository = repository;
|
|
162
|
+
this.mapper = mapper;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Override
|
|
166
|
+
@Transactional(readOnly = true)
|
|
167
|
+
@LogExceptions
|
|
168
|
+
public PagedResponse<ProductResponseDto> handle(FindProductsByCategoryQuery query) {
|
|
169
|
+
Pageable pageable = PageRequest.of(query.page(), query.size(),
|
|
170
|
+
Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy()));
|
|
171
|
+
|
|
172
|
+
Page<Product> page = repository.findByCategoryId(query.categoryId(), pageable);
|
|
173
|
+
List<ProductResponseDto> content = page.getContent().stream()
|
|
174
|
+
.map(mapper::toDto)
|
|
175
|
+
.toList();
|
|
176
|
+
|
|
177
|
+
return PagedResponse.of(content, page.getNumber(), page.getSize(), page.getTotalElements());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 4. Query con filtros múltiples opcionales
|
|
185
|
+
|
|
186
|
+
**Caso:** `FindAllOrders` con filtros por `customerId`, `status`, `fromDate`, `toDate`
|
|
187
|
+
|
|
188
|
+
Cuando hay múltiples filtros opcionales, usa `Specification` para construir queries dinámicas.
|
|
189
|
+
|
|
190
|
+
### Paso 1 — Specification en infraestructura
|
|
191
|
+
|
|
192
|
+
```java
|
|
193
|
+
// infrastructure/database/repositories/OrderJpaSpecification.java
|
|
194
|
+
public class OrderJpaSpecification {
|
|
195
|
+
|
|
196
|
+
public static Specification<OrderJpa> withFilters(
|
|
197
|
+
String customerId, OrderStatus status,
|
|
198
|
+
LocalDateTime fromDate, LocalDateTime toDate) {
|
|
199
|
+
|
|
200
|
+
return (root, query, cb) -> {
|
|
201
|
+
List<Predicate> predicates = new ArrayList<>();
|
|
202
|
+
|
|
203
|
+
if (customerId != null && !customerId.isBlank()) {
|
|
204
|
+
predicates.add(cb.equal(root.get("customerId"), customerId));
|
|
205
|
+
}
|
|
206
|
+
if (status != null) {
|
|
207
|
+
predicates.add(cb.equal(root.get("status"), status));
|
|
208
|
+
}
|
|
209
|
+
if (fromDate != null) {
|
|
210
|
+
predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), fromDate));
|
|
211
|
+
}
|
|
212
|
+
if (toDate != null) {
|
|
213
|
+
predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), toDate));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return cb.and(predicates.toArray(new Predicate[0]));
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Paso 2 — JPA Repository extiende JpaSpecificationExecutor
|
|
223
|
+
|
|
224
|
+
```java
|
|
225
|
+
public interface OrderJpaRepository
|
|
226
|
+
extends JpaRepository<OrderJpa, String>, JpaSpecificationExecutor<OrderJpa> {
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Paso 3 — Repositorio de dominio
|
|
231
|
+
|
|
232
|
+
```java
|
|
233
|
+
Page<Order> findAll(String customerId, OrderStatus status,
|
|
234
|
+
LocalDateTime fromDate, LocalDateTime toDate,
|
|
235
|
+
Pageable pageable);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Paso 4 — Repository Implementation
|
|
239
|
+
|
|
240
|
+
```java
|
|
241
|
+
@Override
|
|
242
|
+
public Page<Order> findAll(String customerId, OrderStatus status,
|
|
243
|
+
LocalDateTime fromDate, LocalDateTime toDate,
|
|
244
|
+
Pageable pageable) {
|
|
245
|
+
Specification<OrderJpa> spec = OrderJpaSpecification.withFilters(
|
|
246
|
+
customerId, status, fromDate, toDate);
|
|
247
|
+
return jpaRepository.findAll(spec, pageable).map(mapper::toDomain);
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Paso 5 — Handler
|
|
252
|
+
|
|
253
|
+
```java
|
|
254
|
+
@Override
|
|
255
|
+
@Transactional(readOnly = true)
|
|
256
|
+
@LogExceptions
|
|
257
|
+
public PagedResponse<OrderResponseDto> handle(FindAllOrdersQuery query) {
|
|
258
|
+
Pageable pageable = PageRequest.of(query.page(), query.size(),
|
|
259
|
+
Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy()));
|
|
260
|
+
|
|
261
|
+
Page<Order> page = repository.findAll(
|
|
262
|
+
query.customerId(), query.status(),
|
|
263
|
+
query.fromDate(), query.toDate(), pageable);
|
|
264
|
+
|
|
265
|
+
List<OrderResponseDto> content = page.getContent().stream()
|
|
266
|
+
.map(mapper::toDto)
|
|
267
|
+
.toList();
|
|
268
|
+
|
|
269
|
+
return PagedResponse.of(content, page.getNumber(), page.getSize(), page.getTotalElements());
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Import para Specification:**
|
|
274
|
+
```java
|
|
275
|
+
import org.springframework.data.jpa.domain.Specification;
|
|
276
|
+
import jakarta.persistence.criteria.Predicate;
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 5. Command de creación con unicidad
|
|
282
|
+
|
|
283
|
+
**Caso:** `CreateProduct` (SKU único), `CreateCustomer` (email único)
|
|
284
|
+
|
|
285
|
+
```java
|
|
286
|
+
@ApplicationComponent
|
|
287
|
+
public class CreateProductCommandHandler
|
|
288
|
+
implements CommandHandler<CreateProductCommand> {
|
|
289
|
+
|
|
290
|
+
private final ProductRepository repository;
|
|
291
|
+
|
|
292
|
+
public CreateProductCommandHandler(ProductRepository repository) {
|
|
293
|
+
this.repository = repository;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@Override
|
|
297
|
+
@Transactional
|
|
298
|
+
@LogExceptions
|
|
299
|
+
public void handle(CreateProductCommand command) {
|
|
300
|
+
// Verificar invariante de unicidad
|
|
301
|
+
repository.findBySku(command.sku()).ifPresent(existing -> {
|
|
302
|
+
throw new DuplicateSkuException(
|
|
303
|
+
"Product with SKU '" + command.sku() + "' already exists");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Crear entidad — constructor de creación (sin id, sin audit, sin readOnly)
|
|
307
|
+
Product entity = new Product(
|
|
308
|
+
command.name(),
|
|
309
|
+
command.description(),
|
|
310
|
+
command.sku(),
|
|
311
|
+
command.categoryId(),
|
|
312
|
+
command.price(),
|
|
313
|
+
command.unit(),
|
|
314
|
+
command.imageUrl()
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
repository.save(entity);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Requiere agregar al repositorio:**
|
|
323
|
+
```java
|
|
324
|
+
Optional<Product> findBySku(String sku);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## 6. Command de actualización
|
|
330
|
+
|
|
331
|
+
**Caso:** `UpdateProduct`, `UpdateCustomer`
|
|
332
|
+
|
|
333
|
+
El patrón de eva4j usa el constructor completo con merge de valores para lograr PATCH semántica **sin setters**.
|
|
334
|
+
|
|
335
|
+
### Si la entidad tiene método `update()` (generado por lifecycle event)
|
|
336
|
+
|
|
337
|
+
```java
|
|
338
|
+
@Override
|
|
339
|
+
@Transactional
|
|
340
|
+
@LogExceptions
|
|
341
|
+
public void handle(UpdateProductCommand command) {
|
|
342
|
+
Product existing = repository.findById(command.id())
|
|
343
|
+
.orElseThrow(() -> new NotFoundException("Product not found with id: " + command.id()));
|
|
344
|
+
|
|
345
|
+
existing.update(
|
|
346
|
+
command.name() != null ? command.name() : existing.getName(),
|
|
347
|
+
command.description() != null ? command.description() : existing.getDescription(),
|
|
348
|
+
command.price() != null ? command.price() : existing.getPrice()
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
repository.save(existing);
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Si NO hay método `update()` — usar constructor completo
|
|
356
|
+
|
|
357
|
+
```java
|
|
358
|
+
@Override
|
|
359
|
+
@Transactional
|
|
360
|
+
@LogExceptions
|
|
361
|
+
public void handle(UpdateProductCommand command) {
|
|
362
|
+
Product existing = repository.findById(command.id())
|
|
363
|
+
.orElseThrow(() -> new NotFoundException("Product not found with id: " + command.id()));
|
|
364
|
+
|
|
365
|
+
// Reconstruir con merge — campos readOnly y audit preservados del existing
|
|
366
|
+
Product updated = new Product(
|
|
367
|
+
existing.getId(),
|
|
368
|
+
command.name() != null ? command.name() : existing.getName(),
|
|
369
|
+
command.description() != null ? command.description() : existing.getDescription(),
|
|
370
|
+
command.sku() != null ? command.sku() : existing.getSku(),
|
|
371
|
+
command.categoryId() != null ? command.categoryId() : existing.getCategoryId(),
|
|
372
|
+
command.price() != null ? command.price() : existing.getPrice(),
|
|
373
|
+
command.unit() != null ? command.unit() : existing.getUnit(),
|
|
374
|
+
command.imageUrl() != null ? command.imageUrl() : existing.getImageUrl(),
|
|
375
|
+
existing.getStatus(), // readOnly — siempre preservar
|
|
376
|
+
existing.getCreatedAt(), // audit — siempre preservar
|
|
377
|
+
existing.getUpdatedAt(), // audit — siempre preservar
|
|
378
|
+
existing.getCreatedBy(), // audit — siempre preservar
|
|
379
|
+
existing.getUpdatedBy(), // audit — siempre preservar
|
|
380
|
+
existing.getDeletedAt() // soft delete — siempre preservar
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
repository.save(updated);
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**Reglas del merge:**
|
|
388
|
+
- Campos normales: `command.x() != null ? command.x() : existing.getX()`
|
|
389
|
+
- Campos `readOnly`: siempre `existing.getX()`
|
|
390
|
+
- Campos de auditoría: siempre `existing.getX()`
|
|
391
|
+
- Campo `deletedAt`: siempre `existing.getDeletedAt()`
|
|
392
|
+
- Campo `id`: siempre `existing.getId()`
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## 7. Command de transición de estado
|
|
397
|
+
|
|
398
|
+
**Caso:** `CancelOrder`, `ConfirmOrder`, `ActivateProduct`, `DeactivateProduct`
|
|
399
|
+
|
|
400
|
+
```java
|
|
401
|
+
@ApplicationComponent
|
|
402
|
+
public class CancelOrderCommandHandler
|
|
403
|
+
implements CommandHandler<CancelOrderCommand> {
|
|
404
|
+
|
|
405
|
+
private final OrderRepository repository;
|
|
406
|
+
|
|
407
|
+
public CancelOrderCommandHandler(OrderRepository repository) {
|
|
408
|
+
this.repository = repository;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
@Override
|
|
412
|
+
@Transactional
|
|
413
|
+
@LogExceptions
|
|
414
|
+
public void handle(CancelOrderCommand command) {
|
|
415
|
+
Order entity = repository.findById(command.id())
|
|
416
|
+
.orElseThrow(() -> new NotFoundException("Order not found with id: " + command.id()));
|
|
417
|
+
|
|
418
|
+
entity.cancel(); // Valida la transición internamente vía el enum
|
|
419
|
+
repository.save(entity);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Cómo funciona internamente:**
|
|
425
|
+
```java
|
|
426
|
+
// En Order.java — generado por eva4j
|
|
427
|
+
public void cancel() {
|
|
428
|
+
this.status = this.status.transitionTo(OrderStatus.CANCELLED);
|
|
429
|
+
// Si hay triggers: raise(new OrderCancelledEvent(this.getId(), ...));
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
El enum `transitionTo()` lanza `InvalidStateTransitionException` si la transición no es válida → manejado como HTTP 409.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## 8. Command con soft delete
|
|
438
|
+
|
|
439
|
+
**Caso:** `DeleteProduct` (con `hasSoftDelete: true`)
|
|
440
|
+
|
|
441
|
+
```java
|
|
442
|
+
@Override
|
|
443
|
+
@Transactional
|
|
444
|
+
@LogExceptions
|
|
445
|
+
public void handle(DeleteProductCommand command) {
|
|
446
|
+
Product entity = repository.findById(command.id())
|
|
447
|
+
.orElseThrow(() -> new NotFoundException("Product not found with id: " + command.id()));
|
|
448
|
+
|
|
449
|
+
entity.softDelete(); // Marca deletedAt = now, lanza si ya estaba eliminado
|
|
450
|
+
repository.save(entity); // Persiste el cambio — NUNCA usar deleteById()
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**NUNCA:**
|
|
455
|
+
```java
|
|
456
|
+
repository.deleteById(command.id()); // ❌ Ignora soft delete
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## 9. Command que emite eventos
|
|
462
|
+
|
|
463
|
+
**Caso:** Use cases que deben publicar domain events post-transacción.
|
|
464
|
+
|
|
465
|
+
Si el evento está declarado con `triggers` o `lifecycle` en `domain.yaml`, el `raise()` ya está generado dentro del método de negocio de la entidad. Solo necesitas llamar al método de negocio:
|
|
466
|
+
|
|
467
|
+
```java
|
|
468
|
+
entity.confirm(); // Internamente hace raise(new OrderConfirmedEvent(...))
|
|
469
|
+
repository.save(entity); // RepositoryImpl publica los eventos pendientes
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Si el evento NO tiene triggers, publícalo manualmente:
|
|
473
|
+
|
|
474
|
+
```java
|
|
475
|
+
entity.raise(new CustomEvent(entity.getId(), LocalDateTime.now()));
|
|
476
|
+
repository.save(entity);
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## 10. Activity de Temporal (light)
|
|
482
|
+
|
|
483
|
+
**Caso:** `CreateOrderFromCart`, `GetCartDetails`, `ClearCart`, `ConfirmOrder`
|
|
484
|
+
|
|
485
|
+
Las actividades light (< 5s) acceden a BD local del módulo. Viven en `infrastructure/adapters/activities/`.
|
|
486
|
+
|
|
487
|
+
**Ubicación de contratos:**
|
|
488
|
+
- **Cross-module** (activity invocada por workflow de **otro** módulo): contrato en `shared/domain/contracts/{thisModule}/`
|
|
489
|
+
- **Local** (activity invocada por workflow del **mismo** módulo): contrato en `{module}/application/ports/` + `{module}/application/dtos/temporal/`
|
|
490
|
+
|
|
491
|
+
**La implementación SIEMPRE vive en:** `{module}/infrastructure/adapters/activities/{Activity}ActivityImpl.java`
|
|
492
|
+
|
|
493
|
+
```java
|
|
494
|
+
@Component
|
|
495
|
+
@RequiredArgsConstructor
|
|
496
|
+
public class CreateOrderFromCartActivityImpl
|
|
497
|
+
implements CreateOrderFromCartActivity, OrdersLightActivity {
|
|
498
|
+
// ^^^^^^^^^^^^^^^^^
|
|
499
|
+
// Marker interface del módulo — determina en qué worker se registra
|
|
500
|
+
|
|
501
|
+
private final OrderRepository repository;
|
|
502
|
+
// Solo repositorios del PROPIO módulo — nunca de otro bounded context
|
|
503
|
+
|
|
504
|
+
@Override
|
|
505
|
+
public CreateOrderFromCartOutput execute(CreateOrderFromCartInput input) {
|
|
506
|
+
// Validar invariantes
|
|
507
|
+
if (input.items() == null || input.items().isEmpty()) {
|
|
508
|
+
throw new EmptyOrderException("Order must have at least one item");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Crear entidad de dominio usando datos del input del workflow
|
|
512
|
+
Order order = new Order(
|
|
513
|
+
input.customerId(),
|
|
514
|
+
input.items().stream()
|
|
515
|
+
.map(item -> new OrderItem(
|
|
516
|
+
item.productId(), item.productName(),
|
|
517
|
+
item.price(), item.quantity()))
|
|
518
|
+
.toList(),
|
|
519
|
+
input.totalAmount(),
|
|
520
|
+
new ShippingAddress(input.street(), input.city(),
|
|
521
|
+
input.neighborhood(), input.zipCode())
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
// Persistir
|
|
525
|
+
Order saved = repository.save(order);
|
|
526
|
+
return new CreateOrderFromCartOutput(saved.getId());
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Diferencias con handlers HTTP:**
|
|
532
|
+
- Clase anotada con `@Component` + `@RequiredArgsConstructor` (no `@ApplicationComponent`)
|
|
533
|
+
- Implementa **dos interfaces**: el contrato `{Activity}Activity` + el marker `{Module}LightActivity`
|
|
534
|
+
- **No** usa `@Transactional` — Temporal gestiona reintentos
|
|
535
|
+
- **No** usa `@LogExceptions` — Temporal captura excepciones para el Saga
|
|
536
|
+
- Input/Output son records del contrato Temporal, no Commands/Queries
|
|
537
|
+
- Puede lanzar excepciones que Temporal captura para compensación (Saga)
|
|
538
|
+
|
|
539
|
+
**Cómo determinar el marker interface:**
|
|
540
|
+
- El task queue en el `WorkFlowImpl` indica la categoría: `*_LIGHT_TASK_QUEUE` → `{Module}LightActivity`, `*_HEAVY_TASK_QUEUE` → `{Module}HeavyActivity`
|
|
541
|
+
- Los markers viven en `{module}/domain/interfaces/{Module}LightActivity.java` y `{Module}HeavyActivity.java`
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## 11. Activity de Temporal (heavy)
|
|
546
|
+
|
|
547
|
+
**Caso:** `ProcessPayment`, `RefundPayment`, `ScheduleDelivery`
|
|
548
|
+
|
|
549
|
+
Actividades heavy (hasta 30s) llaman a servicios externos vía puertos. Se registran en el heavy worker del módulo.
|
|
550
|
+
|
|
551
|
+
```java
|
|
552
|
+
@Component
|
|
553
|
+
@RequiredArgsConstructor
|
|
554
|
+
public class ProcessPaymentActivityImpl
|
|
555
|
+
implements ProcessPaymentActivity, PaymentsHeavyActivity {
|
|
556
|
+
// ^^^^^^^^^^^^^^^^^^^^
|
|
557
|
+
// Heavy marker — registrado en PAYMENTS_HEAVY_TASK_QUEUE
|
|
558
|
+
|
|
559
|
+
private final PaymentRepository paymentRepository;
|
|
560
|
+
private final PaymentGatewayService paymentGateway; // Puerto a servicio externo (Feign)
|
|
561
|
+
|
|
562
|
+
@Override
|
|
563
|
+
public ProcessPaymentOutput execute(ProcessPaymentInput input) {
|
|
564
|
+
// Crear entidad en estado PENDING
|
|
565
|
+
Payment payment = new Payment(input.orderId(), input.amount(), input.currency());
|
|
566
|
+
paymentRepository.save(payment);
|
|
567
|
+
|
|
568
|
+
// Transición a PROCESSING
|
|
569
|
+
payment.startProcessing();
|
|
570
|
+
paymentRepository.save(payment);
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
// Llamar servicio externo (ACL via Feign/puerto)
|
|
574
|
+
GatewayResponse response = paymentGateway.charge(input.amount(), input.currency());
|
|
575
|
+
|
|
576
|
+
// Transición a COMPLETED
|
|
577
|
+
payment.complete(response.getTransactionId());
|
|
578
|
+
paymentRepository.save(payment);
|
|
579
|
+
|
|
580
|
+
return new ProcessPaymentOutput(payment.getId(), "COMPLETED");
|
|
581
|
+
} catch (Exception e) {
|
|
582
|
+
// Transición a FAILED
|
|
583
|
+
payment.fail(e.getMessage());
|
|
584
|
+
paymentRepository.save(payment);
|
|
585
|
+
|
|
586
|
+
throw new PaymentFailedException("Payment processing failed: " + e.getMessage());
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Diferencias con light activities:**
|
|
593
|
+
- Implementa `{Module}HeavyActivity` en lugar de `{Module}LightActivity`
|
|
594
|
+
- El task queue es `*_HEAVY_TASK_QUEUE` (menos workers, más timeout)
|
|
595
|
+
- Suele inyectar puertos a servicios externos además de repositorios
|
|
596
|
+
- El timeout en el `WorkFlowImpl` es mayor (30s vs 5s)
|
|
597
|
+
- Si lanza excepción, Temporal ejecuta la compensación del Saga (ej: `RefundPayment`)
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
## 12. Command sobre entidad secundaria
|
|
602
|
+
|
|
603
|
+
**Caso:** `AddCustomerAddress`, `RemoveCustomerAddress`
|
|
604
|
+
|
|
605
|
+
Cuando operas sobre una entidad secundaria del agregado, siempre accedes a través de la raíz.
|
|
606
|
+
|
|
607
|
+
> **Importante (proyectos existentes):** Verifica que `@OneToMany` en la entidad JPA padre incluya `orphanRemoval = true`. Sin esto, quitar un hijo de la colección con `remove*()` no ejecuta el DELETE en la BD — JPA solo desasocia la referencia en memoria. Proyectos generados con eva4j ≥ 1.0.16 ya lo incluyen automáticamente. En proyectos anteriores, agrégalo manualmente:
|
|
608
|
+
> ```java
|
|
609
|
+
> @OneToMany(mappedBy = "customer", cascade = {...}, orphanRemoval = true, fetch = FetchType.LAZY)
|
|
610
|
+
> ```
|
|
611
|
+
|
|
612
|
+
```java
|
|
613
|
+
@Override
|
|
614
|
+
@Transactional
|
|
615
|
+
@LogExceptions
|
|
616
|
+
public void handle(AddCustomerAddressCommand command) {
|
|
617
|
+
Customer customer = repository.findById(command.customerId())
|
|
618
|
+
.orElseThrow(() -> new NotFoundException("Customer not found"));
|
|
619
|
+
|
|
620
|
+
Address address = new Address(
|
|
621
|
+
command.label(), command.street(), command.city(),
|
|
622
|
+
command.zipCode(), command.isDefault()
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
customer.addAddress(address); // Método en la raíz del agregado
|
|
626
|
+
repository.save(customer);
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**En la entidad raíz:**
|
|
631
|
+
```java
|
|
632
|
+
public void addAddress(Address address) {
|
|
633
|
+
if (address.isDefault()) {
|
|
634
|
+
this.addresses.forEach(a -> a.unsetDefault()); // Solo uno default
|
|
635
|
+
}
|
|
636
|
+
this.addresses.add(address);
|
|
637
|
+
address.assignCustomer(this); // Bidireccionalidad
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## 13. Query con proyección parcial
|
|
644
|
+
|
|
645
|
+
Cuando solo necesitas un subconjunto de campos (performance), crea un DTO específico y un método de repositorio que devuelva la proyección.
|
|
646
|
+
|
|
647
|
+
```java
|
|
648
|
+
// DTO de proyección
|
|
649
|
+
public record ProductSummaryDto(String id, String name, BigDecimal price) {}
|
|
650
|
+
|
|
651
|
+
// En el repositorio de dominio
|
|
652
|
+
List<ProductSummaryDto> findSummaryByCategoryId(String categoryId);
|
|
653
|
+
|
|
654
|
+
// En JPA Repository — proyección nativa
|
|
655
|
+
@Query("SELECT new com.example.app.productCatalog.application.dtos.ProductSummaryDto" +
|
|
656
|
+
"(p.id, p.name, p.price) FROM ProductJpa p WHERE p.categoryId = :categoryId")
|
|
657
|
+
List<ProductSummaryDto> findSummaryByCategoryId(@Param("categoryId") String categoryId);
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
> **Nota:** Este patrón solo se justifica cuando hay métricas de rendimiento que lo requieran. Por defecto, usa el mapper estándar.
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
## 14. Agregar métodos al repositorio
|
|
665
|
+
|
|
666
|
+
Cuando el caso de uso necesita un método que no existe en el repositorio, **siempre modifica 3 archivos** en este orden:
|
|
667
|
+
|
|
668
|
+
### 1. Interfaz de dominio (`domain/repositories/{Entity}Repository.java`)
|
|
669
|
+
```java
|
|
670
|
+
Optional<Product> findBySku(String sku);
|
|
671
|
+
Page<Product> findByCategoryId(String categoryId, Pageable pageable);
|
|
672
|
+
List<Product> findByStatus(ProductStatus status);
|
|
673
|
+
boolean existsBySku(String sku);
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### 2. JPA Repository (`infrastructure/database/repositories/{Entity}JpaRepository.java`)
|
|
677
|
+
```java
|
|
678
|
+
Optional<ProductJpa> findBySku(String sku);
|
|
679
|
+
Page<ProductJpa> findByCategoryId(String categoryId, Pageable pageable);
|
|
680
|
+
List<ProductJpa> findByStatus(ProductStatus status);
|
|
681
|
+
boolean existsBySku(String sku);
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### 3. Repository Implementation (`infrastructure/database/repositories/{Entity}RepositoryImpl.java`)
|
|
685
|
+
```java
|
|
686
|
+
@Override
|
|
687
|
+
public Optional<Product> findBySku(String sku) {
|
|
688
|
+
return jpaRepository.findBySku(sku).map(mapper::toDomain);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
@Override
|
|
692
|
+
public Page<Product> findByCategoryId(String categoryId, Pageable pageable) {
|
|
693
|
+
return jpaRepository.findByCategoryId(categoryId, pageable).map(mapper::toDomain);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
@Override
|
|
697
|
+
public List<Product> findByStatus(ProductStatus status) {
|
|
698
|
+
return jpaRepository.findByStatus(status).stream()
|
|
699
|
+
.map(mapper::toDomain)
|
|
700
|
+
.toList();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
@Override
|
|
704
|
+
public boolean existsBySku(String sku) {
|
|
705
|
+
return jpaRepository.existsBySku(sku);
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**Convenciones de nombres Spring Data:**
|
|
710
|
+
- `findBy{Campo}` — busca por campo exacto
|
|
711
|
+
- `findBy{Campo}And{Campo2}` — busca por combinación
|
|
712
|
+
- `findBy{Campo}OrderBy{Campo2}Asc` — con ordenamiento
|
|
713
|
+
- `existsBy{Campo}` — retorna boolean
|
|
714
|
+
- `countBy{Campo}` — retorna long
|
|
715
|
+
- `deleteBy{Campo}` — elimina por campo (solo si NO hay soft delete)
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## 15. Crear excepciones custom
|
|
720
|
+
|
|
721
|
+
Solo crea excepciones custom cuando el `.md` del módulo las define explícitamente (por ejemplo `DuplicateSkuException`, `InsufficientStockException`).
|
|
722
|
+
|
|
723
|
+
### Ubicación
|
|
724
|
+
```
|
|
725
|
+
{module}/domain/customExceptions/{ExceptionName}.java
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
O si es compartida entre módulos:
|
|
729
|
+
```
|
|
730
|
+
shared/domain/customExceptions/{ExceptionName}.java
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Patrón
|
|
734
|
+
```java
|
|
735
|
+
public class DuplicateSkuException extends BusinessException {
|
|
736
|
+
public DuplicateSkuException(String message) {
|
|
737
|
+
super(message);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### Registrar en HandlerExceptions (si requiere HTTP status diferente)
|
|
743
|
+
|
|
744
|
+
Si la excepción necesita un código HTTP específico distinto al de `BusinessException` (422), agrega un handler:
|
|
745
|
+
|
|
746
|
+
```java
|
|
747
|
+
// En shared/infrastructure/handlerException/HandlerExceptions.java
|
|
748
|
+
@ResponseStatus(HttpStatus.CONFLICT)
|
|
749
|
+
@ExceptionHandler(DuplicateSkuException.class)
|
|
750
|
+
@ResponseBody
|
|
751
|
+
public ErrorResponse onDuplicateSkuException(DuplicateSkuException ex) {
|
|
752
|
+
return new ErrorResponse(
|
|
753
|
+
HttpStatus.CONFLICT.value(),
|
|
754
|
+
"Conflict",
|
|
755
|
+
ex.getMessage()
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
Mapeo estándar: 409 Conflict para duplicados, 400 Bad Request para validaciones, 404 para no encontrado, 422 para reglas de negocio.
|
|
761
|
+
|
|
762
|
+
---
|
|
763
|
+
|
|
764
|
+
## 16. Activity cross-module — leer el workflow como spec
|
|
765
|
+
|
|
766
|
+
**Caso:** Activities cuyo contrato vive en `shared/domain/contracts/{module}/` e implementación en `{module}/infrastructure/adapters/activities/`
|
|
767
|
+
|
|
768
|
+
Cuando el workflow de un módulo orquestador (ej: `shoppingCarts`) invoca activities de otros módulos (ej: `orders`, `inventory`, `payments`), el `WorkFlowImpl` actúa como **spec funcional implícita**.
|
|
769
|
+
|
|
770
|
+
### Cómo leer el workflow como spec
|
|
771
|
+
|
|
772
|
+
```java
|
|
773
|
+
// En PlaceOrderWorkFlowImpl.java (módulo shoppingCarts)
|
|
774
|
+
// Step 4: CreateOrderFromCart (→ orders)
|
|
775
|
+
var createOrderFromCartResult = createOrderFromCartActivity.execute(
|
|
776
|
+
new CreateOrderFromCartInput(
|
|
777
|
+
getCartDetailsResult.customerId(), // ← este dato llega al input
|
|
778
|
+
getCartDetailsResult.items(), // ← lista de items
|
|
779
|
+
getCartDetailsResult.totalAmount(), // ← total calculado
|
|
780
|
+
getCustomerByIdResult.street(), // ← dirección
|
|
781
|
+
getCustomerByIdResult.city(),
|
|
782
|
+
getCustomerByIdResult.neighborhood(),
|
|
783
|
+
getCustomerByIdResult.zipCode()
|
|
784
|
+
)
|
|
785
|
+
);
|
|
786
|
+
// El workflow luego usa: createOrderFromCartResult.orderId() ← este campo DEBE existir en Output
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
**Lo que te dice el workflow:**
|
|
790
|
+
1. **Input:** Qué campos recibe la activity (leer `CreateOrderFromCartInput.java` para la estructura exacta)
|
|
791
|
+
2. **Output:** Qué campos usa el workflow después (leer `CreateOrderFromCartOutput.java`)
|
|
792
|
+
3. **Compensación:** Si hay `saga.addCompensation(() -> ...)`, existe una activity inversa que también debes implementar
|
|
793
|
+
4. **Dependencias de datos:** Qué pasos previos generan los datos que esta activity recibe
|
|
794
|
+
|
|
795
|
+
### Patrón de implementación
|
|
796
|
+
|
|
797
|
+
```java
|
|
798
|
+
@Component
|
|
799
|
+
@RequiredArgsConstructor
|
|
800
|
+
public class CreateOrderFromCartActivityImpl
|
|
801
|
+
implements CreateOrderFromCartActivity, OrdersLightActivity {
|
|
802
|
+
|
|
803
|
+
private final OrderRepository orderRepository;
|
|
804
|
+
|
|
805
|
+
@Override
|
|
806
|
+
public CreateOrderFromCartOutput execute(CreateOrderFromCartInput input) {
|
|
807
|
+
// 1. Construir entidades hijas desde el input (si las hay)
|
|
808
|
+
List<OrderItem> items = input.items().stream()
|
|
809
|
+
.map(item -> new OrderItem(
|
|
810
|
+
item.productId(), item.productName(),
|
|
811
|
+
item.unitPrice(), item.quantity(), item.subtotal()))
|
|
812
|
+
.toList();
|
|
813
|
+
|
|
814
|
+
// 2. Construir Value Objects desde el input (si los hay)
|
|
815
|
+
ShippingAddress address = new ShippingAddress(
|
|
816
|
+
input.street(), input.city(),
|
|
817
|
+
input.neighborhood(), input.zipCode());
|
|
818
|
+
|
|
819
|
+
// 3. Crear entidad raíz del agregado
|
|
820
|
+
Order order = new Order(
|
|
821
|
+
input.customerId(), items,
|
|
822
|
+
input.totalAmount(), address);
|
|
823
|
+
|
|
824
|
+
// 4. Persistir
|
|
825
|
+
Order saved = orderRepository.save(order);
|
|
826
|
+
|
|
827
|
+
// 5. Retornar Output con los campos que el workflow necesita
|
|
828
|
+
return new CreateOrderFromCartOutput(saved.getId());
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
**Reglas para cross-module:**
|
|
834
|
+
- **Nunca** modifiques los archivos en `shared/domain/contracts/` — son contratos generados por eva4j
|
|
835
|
+
- **Solo** modifica el `ActivityImpl` en `{module}/infrastructure/adapters/activities/`
|
|
836
|
+
- **Solo** inyecta repositorios del **propio módulo** — la data de otros módulos llega vía el Input
|
|
837
|
+
- Si necesitas un método de repositorio que no existe, agrégalo en los 3 archivos del propio módulo
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## 17. Activity de compensación (rollback)
|
|
842
|
+
|
|
843
|
+
**Caso:** `ReleaseStock` (compensa `ReserveStock`), `RefundPayment` (compensa `ProcessPayment`), `CancelDelivery` (compensa `ScheduleDelivery`), `RestoreCart` (compensa `ClearCart`)
|
|
844
|
+
|
|
845
|
+
Las activities de compensación deshacen el efecto de una activity principal. Son invocadas automáticamente por el Saga cuando un paso posterior falla.
|
|
846
|
+
|
|
847
|
+
### Cómo identificar pares compensación/principal
|
|
848
|
+
|
|
849
|
+
En el `WorkFlowImpl`, busca el patrón `saga.addCompensation(...)`:
|
|
850
|
+
|
|
851
|
+
```java
|
|
852
|
+
// Activity principal
|
|
853
|
+
reserveStockActivity.execute(new ReserveStockInput(items));
|
|
854
|
+
// Compensación registrada inmediatamente después
|
|
855
|
+
saga.addCompensation(() ->
|
|
856
|
+
releaseStockActivity.execute(new ReleaseStockInput(items))
|
|
857
|
+
);
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
El input de la compensación suele ser **el mismo** que el de la activity principal (o un subconjunto).
|
|
861
|
+
|
|
862
|
+
### Patrón — Compensación que invierte una operación de escritura
|
|
863
|
+
|
|
864
|
+
```java
|
|
865
|
+
@Component
|
|
866
|
+
@RequiredArgsConstructor
|
|
867
|
+
public class ReleaseStockActivityImpl
|
|
868
|
+
implements ReleaseStockActivity, InventoryLightActivity {
|
|
869
|
+
|
|
870
|
+
private final ProductRepository repository;
|
|
871
|
+
|
|
872
|
+
@Override
|
|
873
|
+
public void execute(ReleaseStockInput input) {
|
|
874
|
+
for (StockReservationItem item : input.items()) {
|
|
875
|
+
Product product = repository.findById(item.productId())
|
|
876
|
+
.orElseThrow(() -> new NotFoundException(
|
|
877
|
+
"Product not found: " + item.productId()));
|
|
878
|
+
|
|
879
|
+
product.releaseStock(item.quantity()); // Inverso de reserveStock()
|
|
880
|
+
repository.save(product);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### Patrón — Compensación que revierte una transición de estado
|
|
887
|
+
|
|
888
|
+
```java
|
|
889
|
+
@Component
|
|
890
|
+
@RequiredArgsConstructor
|
|
891
|
+
public class CancelDeliveryActivityImpl
|
|
892
|
+
implements CancelDeliveryActivity, DeliveriesLightActivity {
|
|
893
|
+
|
|
894
|
+
private final DeliveryRepository repository;
|
|
895
|
+
|
|
896
|
+
@Override
|
|
897
|
+
public void execute(CancelDeliveryInput input) {
|
|
898
|
+
Delivery delivery = repository.findByOrderId(input.orderId())
|
|
899
|
+
.orElseThrow(() -> new NotFoundException(
|
|
900
|
+
"Delivery not found for order: " + input.orderId()));
|
|
901
|
+
|
|
902
|
+
delivery.cancel(); // Transición de estado → CANCELLED
|
|
903
|
+
repository.save(delivery);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
### Patrón — Compensación que restaura una snapshot
|
|
909
|
+
|
|
910
|
+
```java
|
|
911
|
+
@Component
|
|
912
|
+
@RequiredArgsConstructor
|
|
913
|
+
public class RestoreCartActivityImpl
|
|
914
|
+
implements RestoreCartActivity, ShoppingCartsLightActivity {
|
|
915
|
+
|
|
916
|
+
private final ShoppingCartRepository repository;
|
|
917
|
+
|
|
918
|
+
@Override
|
|
919
|
+
public void execute(RestoreCartInput input) {
|
|
920
|
+
ShoppingCart cart = repository.findById(input.cartId())
|
|
921
|
+
.orElseThrow(() -> new NotFoundException(
|
|
922
|
+
"Cart not found: " + input.cartId()));
|
|
923
|
+
|
|
924
|
+
cart.restoreItems(); // Revertir el clearItems()
|
|
925
|
+
repository.save(cart);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
**Reglas de compensación:**
|
|
931
|
+
- La compensación debe ser **idempotente** — ejecutarla 2 veces no debe causar error
|
|
932
|
+
- Si el recurso no existe (ya fue eliminado), la compensación debe completar sin lanzar excepción — usar `findById().ifPresent(...)` cuando sea apropiado
|
|
933
|
+
- Verifica que el método de negocio inverso existe en la entidad de dominio; si no, créalo siguiendo las reglas DDD (sin setters, con validación)
|
|
934
|
+
- La compensación tiene el **mismo marker** (Light/Heavy) que la activity principal
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
## 18. Activity void — transición de estado sin retorno
|
|
939
|
+
|
|
940
|
+
**Caso:** `ConfirmOrder`, `MarkOrderCancelled`, `ClearCart`
|
|
941
|
+
|
|
942
|
+
Activities que modifican estado interno de una entidad pero no retornan datos al workflow. El output del contrato es `void`.
|
|
943
|
+
|
|
944
|
+
```java
|
|
945
|
+
@Component
|
|
946
|
+
@RequiredArgsConstructor
|
|
947
|
+
public class ConfirmOrderActivityImpl
|
|
948
|
+
implements ConfirmOrderActivity, OrdersLightActivity {
|
|
949
|
+
|
|
950
|
+
private final OrderRepository repository;
|
|
951
|
+
|
|
952
|
+
@Override
|
|
953
|
+
public void execute(ConfirmOrderInput input) {
|
|
954
|
+
Order order = repository.findById(input.orderId())
|
|
955
|
+
.orElseThrow(() -> new NotFoundException(
|
|
956
|
+
"Order not found with id: " + input.orderId()));
|
|
957
|
+
|
|
958
|
+
order.confirm(input.paymentId()); // Método de negocio + transición de estado
|
|
959
|
+
repository.save(order);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
**Estructura estándar de una activity void:**
|
|
965
|
+
1. Buscar la entidad por ID (desde el Input)
|
|
966
|
+
2. Llamar al método de negocio de la entidad (transición/modificación)
|
|
967
|
+
3. Persistir con `repository.save(entity)`
|
|
968
|
+
4. No retornar nada — el contrato `@ActivityInterface` declara `void execute(...)`
|
|
969
|
+
|
|
970
|
+
**Cuándo el Input tiene campos adicionales (beyond ID):**
|
|
971
|
+
|
|
972
|
+
Algunos métodos de negocio necesitan datos que provienen de pasos previos del workflow:
|
|
973
|
+
|
|
974
|
+
```java
|
|
975
|
+
// ConfirmOrderInput tiene orderId + paymentId
|
|
976
|
+
// paymentId viene del step anterior (ProcessPayment)
|
|
977
|
+
order.confirm(input.paymentId());
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
Siempre revisa el record de Input y el `WorkFlowImpl` para entender de dónde vienen los campos.
|