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.
Files changed (151) hide show
  1. package/AGENTS.md +220 -5
  2. package/DOMAIN_YAML_GUIDE.md +188 -3
  3. package/FUTURE_FEATURES.md +33 -52
  4. package/QUICK_REFERENCE.md +8 -4
  5. package/bin/eva4j.js +70 -2
  6. package/config/defaults.json +1 -0
  7. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  8. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  9. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  10. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  11. package/docs/commands/EVALUATE_SYSTEM.md +290 -10
  12. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  13. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  15. package/docs/commands/INDEX.md +27 -3
  16. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  17. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  18. package/docs/prototype/system/RISKS.md +277 -0
  19. package/docs/prototype/system/customers.yaml +133 -0
  20. package/docs/prototype/system/inventory.yaml +109 -0
  21. package/docs/prototype/system/notifications.yaml +131 -0
  22. package/docs/prototype/system/orders.yaml +241 -0
  23. package/docs/prototype/system/payments.yaml +256 -0
  24. package/docs/prototype/system/products.yaml +168 -0
  25. package/docs/prototype/system/system.yaml +269 -0
  26. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  27. package/examples/domain-events.yaml +26 -0
  28. package/examples/domain-read-models.yaml +113 -0
  29. package/examples/system/customer.yaml +89 -0
  30. package/examples/system/orders.yaml +119 -0
  31. package/examples/system/product.yaml +27 -0
  32. package/examples/system/system.yaml +80 -0
  33. package/package.json +1 -1
  34. package/read-model-spec.md +664 -0
  35. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  36. package/src/agents/design-gap-analyst.agent.md +383 -0
  37. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  38. package/src/agents/design-reviewer.agent.md +34 -5
  39. package/src/agents/implement-use-cases.prompt.md +179 -0
  40. package/src/agents/ux-gap-analyst.agent.md +412 -0
  41. package/src/commands/add-rabbitmq-client.js +261 -0
  42. package/src/commands/add-temporal-client.js +22 -2
  43. package/src/commands/build.js +267 -11
  44. package/src/commands/evaluate-system.js +700 -13
  45. package/src/commands/generate-entities.js +560 -24
  46. package/src/commands/generate-http-exchange.js +3 -0
  47. package/src/commands/generate-kafka-event.js +3 -0
  48. package/src/commands/generate-kafka-listener.js +3 -0
  49. package/src/commands/generate-rabbitmq-event.js +665 -0
  50. package/src/commands/generate-rabbitmq-listener.js +205 -0
  51. package/src/commands/generate-record.js +2 -2
  52. package/src/commands/generate-resource.js +4 -1
  53. package/src/commands/generate-temporal-activity.js +970 -33
  54. package/src/commands/generate-temporal-flow.js +98 -38
  55. package/src/commands/generate-temporal-system.js +708 -0
  56. package/src/commands/generate-usecase.js +4 -1
  57. package/src/skills/build-system-yaml/SKILL.md +343 -2
  58. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
  59. package/src/skills/build-system-yaml/references/module-spec.md +90 -9
  60. package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
  61. package/src/skills/build-temporal-system/SKILL.md +752 -0
  62. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  63. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  64. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  65. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  66. package/src/skills/implement-use-case/SKILL.md +350 -0
  67. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  68. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  69. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  70. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  71. package/src/utils/bounded-context-diagram.js +844 -0
  72. package/src/utils/config-manager.js +4 -2
  73. package/src/utils/domain-validator.js +495 -17
  74. package/src/utils/naming.js +20 -0
  75. package/src/utils/system-validator.js +169 -11
  76. package/src/utils/system-yaml-parser.js +318 -0
  77. package/src/utils/temporal-validator.js +497 -0
  78. package/src/utils/validator.js +3 -1
  79. package/src/utils/yaml-to-entity.js +281 -9
  80. package/templates/aggregate/AggregateRepository.java.ejs +4 -0
  81. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
  82. package/templates/aggregate/AggregateRoot.java.ejs +38 -4
  83. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  84. package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
  85. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  86. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  87. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  88. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  89. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  90. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  91. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  92. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  93. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  94. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  95. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  96. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  97. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  98. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  99. package/templates/base/root/AGENTS.md.ejs +1 -1
  100. package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
  101. package/templates/crud/EndpointsController.java.ejs +1 -1
  102. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  103. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  104. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  105. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  106. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  107. package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
  108. package/templates/evaluate/report.html.ejs +1447 -90
  109. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  110. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  111. package/templates/ports/PortAclMapper.java.ejs +35 -0
  112. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  113. package/templates/ports/PortFeignClient.java.ejs +4 -0
  114. package/templates/ports/PortResponseDto.java.ejs +1 -1
  115. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  116. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  117. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  118. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  119. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  120. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  121. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  122. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  123. package/templates/read-model/ReadModelDomain.java.ejs +46 -0
  124. package/templates/read-model/ReadModelJpa.java.ejs +58 -0
  125. package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
  126. package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
  127. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  128. package/templates/read-model/ReadModelRepository.java.ejs +42 -0
  129. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
  130. package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
  131. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  132. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  133. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  134. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  135. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  136. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  137. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  138. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  139. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  140. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  141. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  142. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  143. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  144. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  145. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  146. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  147. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  148. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  149. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  150. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  151. 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.