eva4j 1.0.17 → 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 (134) hide show
  1. package/AGENTS.md +2 -0
  2. package/DOMAIN_YAML_GUIDE.md +3 -1
  3. package/QUICK_REFERENCE.md +8 -4
  4. package/bin/eva4j.js +70 -2
  5. package/config/defaults.json +1 -0
  6. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  7. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  8. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  9. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  10. package/docs/commands/EVALUATE_SYSTEM.md +272 -8
  11. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  12. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  13. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  14. package/docs/commands/INDEX.md +27 -3
  15. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  16. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  17. package/docs/prototype/system/RISKS.md +277 -0
  18. package/docs/prototype/system/customers.yaml +133 -0
  19. package/docs/prototype/system/inventory.yaml +109 -0
  20. package/docs/prototype/system/notifications.yaml +131 -0
  21. package/docs/prototype/system/orders.yaml +241 -0
  22. package/docs/prototype/system/payments.yaml +256 -0
  23. package/docs/prototype/system/products.yaml +168 -0
  24. package/docs/prototype/system/system.yaml +269 -0
  25. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  26. package/examples/domain-read-models.yaml +2 -2
  27. package/examples/system/customer.yaml +89 -0
  28. package/examples/system/orders.yaml +119 -0
  29. package/examples/system/product.yaml +27 -0
  30. package/examples/system/system.yaml +80 -0
  31. package/package.json +1 -1
  32. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  33. package/src/agents/design-gap-analyst.agent.md +383 -0
  34. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  35. package/src/agents/design-reviewer.agent.md +31 -5
  36. package/src/agents/implement-use-cases.prompt.md +179 -0
  37. package/src/agents/ux-gap-analyst.agent.md +412 -0
  38. package/src/commands/add-rabbitmq-client.js +261 -0
  39. package/src/commands/add-temporal-client.js +22 -2
  40. package/src/commands/build.js +267 -11
  41. package/src/commands/evaluate-system.js +700 -13
  42. package/src/commands/generate-entities.js +308 -16
  43. package/src/commands/generate-rabbitmq-event.js +665 -0
  44. package/src/commands/generate-rabbitmq-listener.js +205 -0
  45. package/src/commands/generate-temporal-activity.js +968 -34
  46. package/src/commands/generate-temporal-flow.js +95 -38
  47. package/src/commands/generate-temporal-system.js +708 -0
  48. package/src/skills/build-system-yaml/SKILL.md +222 -2
  49. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +50 -4
  50. package/src/skills/build-system-yaml/references/module-spec.md +57 -8
  51. package/src/skills/build-temporal-system/SKILL.md +752 -0
  52. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  53. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  54. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  55. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  56. package/src/skills/implement-use-case/SKILL.md +350 -0
  57. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  58. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  59. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  60. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  61. package/src/utils/bounded-context-diagram.js +844 -0
  62. package/src/utils/domain-validator.js +266 -4
  63. package/src/utils/naming.js +10 -0
  64. package/src/utils/system-validator.js +169 -11
  65. package/src/utils/system-yaml-parser.js +318 -0
  66. package/src/utils/temporal-validator.js +497 -0
  67. package/src/utils/yaml-to-entity.js +10 -7
  68. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  69. package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
  70. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  71. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  72. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  73. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  74. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  75. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  76. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  77. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  78. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  79. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  80. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  81. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  82. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  83. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  84. package/templates/base/root/AGENTS.md.ejs +1 -1
  85. package/templates/crud/EndpointsController.java.ejs +1 -1
  86. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  87. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  88. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  89. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  90. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  91. package/templates/evaluate/report.html.ejs +1447 -90
  92. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  93. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  94. package/templates/ports/PortAclMapper.java.ejs +35 -0
  95. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  96. package/templates/ports/PortFeignClient.java.ejs +4 -0
  97. package/templates/ports/PortResponseDto.java.ejs +1 -1
  98. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  99. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  100. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  101. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  102. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  103. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  104. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  105. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  106. package/templates/read-model/ReadModelJpa.java.ejs +1 -1
  107. package/templates/read-model/ReadModelJpaRepository.java.ejs +2 -0
  108. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  109. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +9 -5
  110. package/templates/read-model/ReadModelSyncHandler.java.ejs +2 -0
  111. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  112. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  113. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  114. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  115. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  116. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  117. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  118. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  119. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  120. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  121. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  122. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  123. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  124. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  125. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  126. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  127. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  128. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  129. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  130. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  131. package/COMMAND_EVALUATION.md +0 -911
  132. package/test-c2010.js +0 -49
  133. package/test-update-compat.js +0 -109
  134. package/test-update-lifecycle.js +0 -121
@@ -0,0 +1,1380 @@
1
+ # Guía práctica: Camunda DMN desde cero
2
+
3
+ Guía paso a paso para aprender DMN (Decision Model and Notation) con Camunda Platform, desde levantar el servidor hasta integrarlo con Spring Boot.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Levantar Camunda con Docker Compose](#1-levantar-camunda-con-docker-compose)
10
+ 2. [Conceptos fundamentales de DMN](#2-conceptos-fundamentales-de-dmn)
11
+ 3. [Ejemplo 1 — Aprobación de crédito (FIRST)](#3-ejemplo-1--aprobación-de-crédito-first)
12
+ 4. [Ejemplo 2 — Beneficios de membresía (COLLECT)](#4-ejemplo-2--beneficios-de-membresía-collect)
13
+ 5. [Ejemplo 3 — Precio dinámico con FEEL (UNIQUE)](#5-ejemplo-3--precio-dinámico-con-feel-unique)
14
+ 6. [Ejemplo 4 — Cadena de decisiones DRG](#6-ejemplo-4--cadena-de-decisiones-drg)
15
+ 7. [Ejemplo 5 — Asignación de tickets (RULE ORDER)](#7-ejemplo-5--asignación-de-tickets-rule-order)
16
+ 8. [API REST de Camunda — Referencia completa](#8-api-rest-de-camunda--referencia-completa)
17
+ 9. [Consumir desde Spring Boot (sin eva4j)](#9-consumir-desde-spring-boot-sin-eva4j)
18
+ 10. [Integración futura con eva4j](#10-integración-futura-con-eva4j)
19
+
20
+ ---
21
+
22
+ ## 1. Levantar Camunda con Docker Compose
23
+
24
+ ### docker-compose.yml
25
+
26
+ ```yaml
27
+ version: '3.8'
28
+
29
+ services:
30
+ camunda:
31
+ image: camunda/camunda-bpm-platform:7.21.0
32
+ container_name: camunda-engine
33
+ ports:
34
+ - "8090:8080"
35
+ environment:
36
+ - DB_DRIVER=org.h2.Driver
37
+ - DB_URL=jdbc:h2:./camundadb;DB_CLOSE_DELAY=-1
38
+ - DB_USERNAME=sa
39
+ - DB_PASSWORD=sa
40
+ - CAMUNDA_BPM_ADMIN_USER_ID=admin
41
+ - CAMUNDA_BPM_ADMIN_USER_PASSWORD=admin
42
+ volumes:
43
+ - camunda-data:/camunda/camundadb
44
+ - ./deployments:/camunda/configuration/resources
45
+
46
+ volumes:
47
+ camunda-data:
48
+ ```
49
+
50
+ ### Comandos
51
+
52
+ ```bash
53
+ # Preparar directorio de reglas y levantar
54
+ mkdir -p deployments
55
+ docker-compose up -d
56
+
57
+ # Verificar logs
58
+ docker logs -f camunda-engine
59
+
60
+ # Detener
61
+ docker-compose down
62
+ ```
63
+
64
+ ### URLs disponibles
65
+
66
+ | URL | Propósito |
67
+ |-----|-----------|
68
+ | `http://localhost:8090/camunda/app/welcome/` | Portal principal (login: `admin` / `admin`) |
69
+ | `http://localhost:8090/camunda/app/cockpit/` | Monitoreo de procesos y decisiones |
70
+ | `http://localhost:8090/camunda/app/tasklist/` | Lista de tareas humanas |
71
+ | `http://localhost:8090/engine-rest/` | REST API para integración programática |
72
+
73
+ ---
74
+
75
+ ## 2. Conceptos fundamentales de DMN
76
+
77
+ ### Anatomía de una Decision Table
78
+
79
+ ```
80
+ ┌─────────────────────────────────────────────────┐
81
+ │ DMN DECISION │
82
+ │ │
83
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
84
+ │ │ INPUT │ → │ RULES │ → │ OUTPUT │ │
85
+ │ │ (datos) │ │ (tabla) │ │ (result) │ │
86
+ │ └──────────┘ └──────────┘ └──────────┘ │
87
+ │ │
88
+ │ Hit Policy: cómo se seleccionan las filas │
89
+ │ Lenguaje: FEEL (Friendly Enough Expression) │
90
+ └─────────────────────────────────────────────────┘
91
+ ```
92
+
93
+ ### Hit Policies
94
+
95
+ Determinan **cuántas filas** del resultado se retornan y en qué orden:
96
+
97
+ | Policy | Símbolo | Comportamiento | Cuándo usarla |
98
+ |--------|:-------:|----------------|---------------|
99
+ | **UNIQUE** | U | Exactamente una regla debe coincidir | Clasificaciones mutuamente excluyentes |
100
+ | **FIRST** | F | Primera regla que coincida (orden importa) | Reglas con prioridad / fallbacks |
101
+ | **RULE ORDER** | R | Todas las que coinciden, en orden de tabla | Pasos secuenciales, listas priorizadas |
102
+ | **COLLECT** | C | Todas las que coinciden (agregable: SUM, MIN, MAX, COUNT) | Acumular beneficios, permisos, coberturas |
103
+ | **ANY** | A | Varias reglas pueden coincidir si dan el mismo resultado | Validación de consistencia |
104
+
105
+ ### FEEL — Expresiones en celdas
106
+
107
+ FEEL (Friendly Enough Expression Language) es el lenguaje estándar de DMN:
108
+
109
+ | Expresión | Significado | Ejemplo |
110
+ |-----------|-------------|---------|
111
+ | `"HIGH"` | Igualdad exacta | String match |
112
+ | `> 1000` | Mayor que | Numéricos |
113
+ | `[18..65]` | Rango inclusivo | Edad entre 18 y 65 |
114
+ | `< date("2025-01-01")` | Comparación de fecha | Antes de 2025 |
115
+ | `"A","B","C"` | Lista de valores | Cualquiera de los tres |
116
+ | `not("X")` | Negación | Cualquier cosa excepto X |
117
+ | *(vacío)* | Cualquier valor (wildcard) | Sin restricción |
118
+
119
+ ### Tipos de datos soportados
120
+
121
+ | Tipo DMN | Ejemplo | Notas |
122
+ |----------|---------|-------|
123
+ | `string` | `"PREMIUM"` | Siempre entre comillas en reglas |
124
+ | `integer` | `42` | Enteros |
125
+ | `long` | `100000000` | Enteros grandes |
126
+ | `double` | `0.085` | Decimales |
127
+ | `boolean` | `true` / `false` | |
128
+ | `date` | `date("2025-06-15")` | ISO 8601 en FEEL |
129
+
130
+ ---
131
+
132
+ ## 3. Ejemplo 1 — Aprobación de crédito (FIRST)
133
+
134
+ **Escenario:** Un banco evalúa solicitudes de crédito personal según el score crediticio, ingreso mensual y monto solicitado. Se usa `FIRST` porque las reglas tienen prioridad descendente — la primera que coincide gana.
135
+
136
+ ### Tabla de decisión visual
137
+
138
+ | # | Score Crediticio | Ingreso Mensual | Monto Solicitado | → Decisión | → Tasa Interés | → Requiere Revisión |
139
+ |---|:---:|:---:|:---:|:---:|:---:|:---:|
140
+ | 1 | < 500 | — | — | RECHAZADO | — | false |
141
+ | 2 | [500..650] | — | > 500000 | RECHAZADO | — | true |
142
+ | 3 | [500..650] | >= 30000 | — | APROBADO_CONDICIONAL | 18.5 | true |
143
+ | 4 | [651..750] | — | — | APROBADO | 14.0 | false |
144
+ | 5 | > 750 | — | — | APROBADO | 10.5 | false |
145
+ | 6 | — | — | — | PENDIENTE_REVISION | — | true |
146
+
147
+ ### Archivo DMN
148
+
149
+ Crear `deployments/credit-approval.dmn`:
150
+
151
+ ```xml
152
+ <?xml version="1.0" encoding="UTF-8"?>
153
+ <definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
154
+ id="credit-approval" name="Credit Approval"
155
+ namespace="http://camunda.org/schema/1.0/dmn">
156
+
157
+ <decision id="evaluateCredit" name="Evaluate Credit Application">
158
+ <decisionTable id="dt_credit" hitPolicy="FIRST">
159
+
160
+ <!-- INPUTS -->
161
+ <input id="i_score" label="Credit Score">
162
+ <inputExpression id="ie_score" typeRef="integer">
163
+ <text>creditScore</text>
164
+ </inputExpression>
165
+ </input>
166
+
167
+ <input id="i_income" label="Monthly Income">
168
+ <inputExpression id="ie_income" typeRef="double">
169
+ <text>monthlyIncome</text>
170
+ </inputExpression>
171
+ </input>
172
+
173
+ <input id="i_amount" label="Requested Amount">
174
+ <inputExpression id="ie_amount" typeRef="double">
175
+ <text>requestedAmount</text>
176
+ </inputExpression>
177
+ </input>
178
+
179
+ <!-- OUTPUTS -->
180
+ <output id="o_decision" label="Decision" name="decision" typeRef="string">
181
+ <outputValues>
182
+ <text>"RECHAZADO","APROBADO_CONDICIONAL","APROBADO","PENDIENTE_REVISION"</text>
183
+ </outputValues>
184
+ </output>
185
+
186
+ <output id="o_rate" label="Interest Rate %" name="interestRate" typeRef="double"/>
187
+
188
+ <output id="o_review" label="Requires Review" name="requiresReview" typeRef="boolean"/>
189
+
190
+ <!-- RULES -->
191
+
192
+ <!-- Rule 1: Score muy bajo → rechazo directo -->
193
+ <rule id="r1">
194
+ <description>Score crediticio muy bajo — rechazo automático</description>
195
+ <inputEntry id="r1_i1"><text>&lt; 500</text></inputEntry>
196
+ <inputEntry id="r1_i2"><text></text></inputEntry>
197
+ <inputEntry id="r1_i3"><text></text></inputEntry>
198
+ <outputEntry id="r1_o1"><text>"RECHAZADO"</text></outputEntry>
199
+ <outputEntry id="r1_o2"><text></text></outputEntry>
200
+ <outputEntry id="r1_o3"><text>false</text></outputEntry>
201
+ </rule>
202
+
203
+ <!-- Rule 2: Score medio-bajo + monto alto → rechazo con revisión -->
204
+ <rule id="r2">
205
+ <description>Score medio-bajo con monto elevado</description>
206
+ <inputEntry id="r2_i1"><text>[500..650]</text></inputEntry>
207
+ <inputEntry id="r2_i2"><text></text></inputEntry>
208
+ <inputEntry id="r2_i3"><text>&gt; 500000</text></inputEntry>
209
+ <outputEntry id="r2_o1"><text>"RECHAZADO"</text></outputEntry>
210
+ <outputEntry id="r2_o2"><text></text></outputEntry>
211
+ <outputEntry id="r2_o3"><text>true</text></outputEntry>
212
+ </rule>
213
+
214
+ <!-- Rule 3: Score medio-bajo pero buen ingreso → aprobado condicional -->
215
+ <rule id="r3">
216
+ <description>Score medio-bajo con ingreso suficiente</description>
217
+ <inputEntry id="r3_i1"><text>[500..650]</text></inputEntry>
218
+ <inputEntry id="r3_i2"><text>&gt;= 30000</text></inputEntry>
219
+ <inputEntry id="r3_i3"><text></text></inputEntry>
220
+ <outputEntry id="r3_o1"><text>"APROBADO_CONDICIONAL"</text></outputEntry>
221
+ <outputEntry id="r3_o2"><text>18.5</text></outputEntry>
222
+ <outputEntry id="r3_o3"><text>true</text></outputEntry>
223
+ </rule>
224
+
225
+ <!-- Rule 4: Score bueno → aprobado -->
226
+ <rule id="r4">
227
+ <description>Score crediticio bueno</description>
228
+ <inputEntry id="r4_i1"><text>[651..750]</text></inputEntry>
229
+ <inputEntry id="r4_i2"><text></text></inputEntry>
230
+ <inputEntry id="r4_i3"><text></text></inputEntry>
231
+ <outputEntry id="r4_o1"><text>"APROBADO"</text></outputEntry>
232
+ <outputEntry id="r4_o2"><text>14.0</text></outputEntry>
233
+ <outputEntry id="r4_o3"><text>false</text></outputEntry>
234
+ </rule>
235
+
236
+ <!-- Rule 5: Score excelente → aprobado con mejor tasa -->
237
+ <rule id="r5">
238
+ <description>Score crediticio excelente</description>
239
+ <inputEntry id="r5_i1"><text>&gt; 750</text></inputEntry>
240
+ <inputEntry id="r5_i2"><text></text></inputEntry>
241
+ <inputEntry id="r5_i3"><text></text></inputEntry>
242
+ <outputEntry id="r5_o1"><text>"APROBADO"</text></outputEntry>
243
+ <outputEntry id="r5_o2"><text>10.5</text></outputEntry>
244
+ <outputEntry id="r5_o3"><text>false</text></outputEntry>
245
+ </rule>
246
+
247
+ <!-- Rule 6: Default — caso no contemplado -->
248
+ <rule id="r6">
249
+ <description>Caso no contemplado — revisión manual</description>
250
+ <inputEntry id="r6_i1"><text></text></inputEntry>
251
+ <inputEntry id="r6_i2"><text></text></inputEntry>
252
+ <inputEntry id="r6_i3"><text></text></inputEntry>
253
+ <outputEntry id="r6_o1"><text>"PENDIENTE_REVISION"</text></outputEntry>
254
+ <outputEntry id="r6_o2"><text></text></outputEntry>
255
+ <outputEntry id="r6_o3"><text>true</text></outputEntry>
256
+ </rule>
257
+
258
+ </decisionTable>
259
+ </decision>
260
+ </definitions>
261
+ ```
262
+
263
+ ### Deploy y pruebas
264
+
265
+ ```bash
266
+ # Reiniciar para cargar la decisión
267
+ docker-compose restart camunda
268
+
269
+ # Test 1: Score excelente → APROBADO, tasa 10.5%
270
+ curl -s -X POST \
271
+ http://localhost:8090/engine-rest/decision-definition/key/evaluateCredit/evaluate \
272
+ -H "Content-Type: application/json" \
273
+ -d '{
274
+ "variables": {
275
+ "creditScore": { "value": 780, "type": "Integer" },
276
+ "monthlyIncome": { "value": 45000, "type": "Double" },
277
+ "requestedAmount": { "value": 200000, "type": "Double" }
278
+ }
279
+ }'
280
+ # → [{"decision":{"value":"APROBADO"},"interestRate":{"value":10.5},"requiresReview":{"value":false}}]
281
+
282
+ # Test 2: Score bajo → RECHAZADO
283
+ curl -s -X POST \
284
+ http://localhost:8090/engine-rest/decision-definition/key/evaluateCredit/evaluate \
285
+ -H "Content-Type: application/json" \
286
+ -d '{
287
+ "variables": {
288
+ "creditScore": { "value": 420, "type": "Integer" },
289
+ "monthlyIncome": { "value": 15000, "type": "Double" },
290
+ "requestedAmount": { "value": 100000, "type": "Double" }
291
+ }
292
+ }'
293
+ # → [{"decision":{"value":"RECHAZADO"},"requiresReview":{"value":false}}]
294
+
295
+ # Test 3: Score medio + buen ingreso → APROBADO_CONDICIONAL, tasa 18.5%
296
+ curl -s -X POST \
297
+ http://localhost:8090/engine-rest/decision-definition/key/evaluateCredit/evaluate \
298
+ -H "Content-Type: application/json" \
299
+ -d '{
300
+ "variables": {
301
+ "creditScore": { "value": 580, "type": "Integer" },
302
+ "monthlyIncome": { "value": 50000, "type": "Double" },
303
+ "requestedAmount": { "value": 150000, "type": "Double" }
304
+ }
305
+ }'
306
+ # → [{"decision":{"value":"APROBADO_CONDICIONAL"},"interestRate":{"value":18.5},"requiresReview":{"value":true}}]
307
+
308
+ # Test 4: Score medio + monto muy alto → RECHAZADO con revisión
309
+ curl -s -X POST \
310
+ http://localhost:8090/engine-rest/decision-definition/key/evaluateCredit/evaluate \
311
+ -H "Content-Type: application/json" \
312
+ -d '{
313
+ "variables": {
314
+ "creditScore": { "value": 600, "type": "Integer" },
315
+ "monthlyIncome": { "value": 25000, "type": "Double" },
316
+ "requestedAmount": { "value": 800000, "type": "Double" }
317
+ }
318
+ }'
319
+ # → [{"decision":{"value":"RECHAZADO"},"requiresReview":{"value":true}}]
320
+ ```
321
+
322
+ ---
323
+
324
+ ## 4. Ejemplo 2 — Beneficios de membresía (COLLECT)
325
+
326
+ **Escenario:** Una plataforma de e-commerce determina qué beneficios obtiene un cliente según su nivel de membresía, antigüedad y gasto acumulado. Se usa `COLLECT` porque un cliente puede recibir **múltiples beneficios simultáneamente**.
327
+
328
+ ### Tabla de decisión visual
329
+
330
+ | # | Nivel Membresía | Antigüedad (años) | Gasto Acumulado | → Beneficio | → Valor |
331
+ |---|:---:|:---:|:---:|:---:|:---:|
332
+ | 1 | — | — | — | ENVIO_GRATIS_BASICO | 100 |
333
+ | 2 | GOLD, PLATINUM | — | — | ENVIO_GRATIS_TOTAL | 100 |
334
+ | 3 | — | >= 2 | — | DESCUENTO_ANIVERSARIO | 5 |
335
+ | 4 | — | — | >= 50000 | DESCUENTO_VOLUMEN | 8 |
336
+ | 5 | PLATINUM | — | — | ACCESO_PREVENTAS | 100 |
337
+ | 6 | GOLD, PLATINUM | >= 3 | — | SOPORTE_PRIORITARIO | 100 |
338
+ | 7 | — | — | >= 100000 | GIFT_CARD_ANUAL | 500 |
339
+
340
+ ### Archivo DMN
341
+
342
+ Crear `deployments/membership-benefits.dmn`:
343
+
344
+ ```xml
345
+ <?xml version="1.0" encoding="UTF-8"?>
346
+ <definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
347
+ id="membership-benefits" name="Membership Benefits"
348
+ namespace="http://camunda.org/schema/1.0/dmn">
349
+
350
+ <decision id="determineBenefits" name="Determine Membership Benefits">
351
+ <decisionTable id="dt_benefits" hitPolicy="COLLECT">
352
+
353
+ <!-- INPUTS -->
354
+ <input id="i_level" label="Membership Level">
355
+ <inputExpression id="ie_level" typeRef="string">
356
+ <text>membershipLevel</text>
357
+ </inputExpression>
358
+ <inputValues><text>"BASIC","SILVER","GOLD","PLATINUM"</text></inputValues>
359
+ </input>
360
+
361
+ <input id="i_years" label="Years as Member">
362
+ <inputExpression id="ie_years" typeRef="integer">
363
+ <text>memberYears</text>
364
+ </inputExpression>
365
+ </input>
366
+
367
+ <input id="i_spent" label="Total Spent">
368
+ <inputExpression id="ie_spent" typeRef="double">
369
+ <text>totalSpent</text>
370
+ </inputExpression>
371
+ </input>
372
+
373
+ <!-- OUTPUTS -->
374
+ <output id="o_benefit" label="Benefit" name="benefit" typeRef="string"/>
375
+ <output id="o_value" label="Value" name="benefitValue" typeRef="double"/>
376
+
377
+ <!-- RULES — todos se evalúan; se acumulan los que coinciden -->
378
+
379
+ <!-- Rule 1: Envío gratis básico para todos -->
380
+ <rule id="r1">
381
+ <description>Todos los miembros tienen envío gratis en compras menores</description>
382
+ <inputEntry><text></text></inputEntry>
383
+ <inputEntry><text></text></inputEntry>
384
+ <inputEntry><text></text></inputEntry>
385
+ <outputEntry><text>"ENVIO_GRATIS_BASICO"</text></outputEntry>
386
+ <outputEntry><text>100</text></outputEntry>
387
+ </rule>
388
+
389
+ <!-- Rule 2: Envío gratis total para Gold/Platinum -->
390
+ <rule id="r2">
391
+ <description>Envío gratis sin límite de monto</description>
392
+ <inputEntry><text>"GOLD","PLATINUM"</text></inputEntry>
393
+ <inputEntry><text></text></inputEntry>
394
+ <inputEntry><text></text></inputEntry>
395
+ <outputEntry><text>"ENVIO_GRATIS_TOTAL"</text></outputEntry>
396
+ <outputEntry><text>100</text></outputEntry>
397
+ </rule>
398
+
399
+ <!-- Rule 3: Descuento por antigüedad -->
400
+ <rule id="r3">
401
+ <description>5% descuento a partir de 2 años como miembro</description>
402
+ <inputEntry><text></text></inputEntry>
403
+ <inputEntry><text>&gt;= 2</text></inputEntry>
404
+ <inputEntry><text></text></inputEntry>
405
+ <outputEntry><text>"DESCUENTO_ANIVERSARIO"</text></outputEntry>
406
+ <outputEntry><text>5</text></outputEntry>
407
+ </rule>
408
+
409
+ <!-- Rule 4: Descuento por volumen de compra -->
410
+ <rule id="r4">
411
+ <description>8% descuento para clientes con alto gasto acumulado</description>
412
+ <inputEntry><text></text></inputEntry>
413
+ <inputEntry><text></text></inputEntry>
414
+ <inputEntry><text>&gt;= 50000</text></inputEntry>
415
+ <outputEntry><text>"DESCUENTO_VOLUMEN"</text></outputEntry>
416
+ <outputEntry><text>8</text></outputEntry>
417
+ </rule>
418
+
419
+ <!-- Rule 5: Acceso a preventas exclusivas -->
420
+ <rule id="r5">
421
+ <description>Solo Platinum accede a preventas</description>
422
+ <inputEntry><text>"PLATINUM"</text></inputEntry>
423
+ <inputEntry><text></text></inputEntry>
424
+ <inputEntry><text></text></inputEntry>
425
+ <outputEntry><text>"ACCESO_PREVENTAS"</text></outputEntry>
426
+ <outputEntry><text>100</text></outputEntry>
427
+ </rule>
428
+
429
+ <!-- Rule 6: Soporte prioritario -->
430
+ <rule id="r6">
431
+ <description>Gold/Platinum con 3+ años: soporte prioritario</description>
432
+ <inputEntry><text>"GOLD","PLATINUM"</text></inputEntry>
433
+ <inputEntry><text>&gt;= 3</text></inputEntry>
434
+ <inputEntry><text></text></inputEntry>
435
+ <outputEntry><text>"SOPORTE_PRIORITARIO"</text></outputEntry>
436
+ <outputEntry><text>100</text></outputEntry>
437
+ </rule>
438
+
439
+ <!-- Rule 7: Gift card anual por alto gasto -->
440
+ <rule id="r7">
441
+ <description>Gift card de $500 para clientes con 100k+ de gasto</description>
442
+ <inputEntry><text></text></inputEntry>
443
+ <inputEntry><text></text></inputEntry>
444
+ <inputEntry><text>&gt;= 100000</text></inputEntry>
445
+ <outputEntry><text>"GIFT_CARD_ANUAL"</text></outputEntry>
446
+ <outputEntry><text>500</text></outputEntry>
447
+ </rule>
448
+
449
+ </decisionTable>
450
+ </decision>
451
+ </definitions>
452
+ ```
453
+
454
+ ### Deploy y pruebas
455
+
456
+ ```bash
457
+ docker-compose restart camunda
458
+
459
+ # Test 1: Cliente Platinum, 5 años, $120k gastados → TODOS los beneficios
460
+ curl -s -X POST \
461
+ http://localhost:8090/engine-rest/decision-definition/key/determineBenefits/evaluate \
462
+ -H "Content-Type: application/json" \
463
+ -d '{
464
+ "variables": {
465
+ "membershipLevel": { "value": "PLATINUM", "type": "String" },
466
+ "memberYears": { "value": 5, "type": "Integer" },
467
+ "totalSpent": { "value": 120000, "type": "Double" }
468
+ }
469
+ }'
470
+ # → 7 beneficios (todos aplican)
471
+
472
+ # Test 2: Cliente Basic, 1 año, $10k → solo envío gratis básico
473
+ curl -s -X POST \
474
+ http://localhost:8090/engine-rest/decision-definition/key/determineBenefits/evaluate \
475
+ -H "Content-Type: application/json" \
476
+ -d '{
477
+ "variables": {
478
+ "membershipLevel": { "value": "BASIC", "type": "String" },
479
+ "memberYears": { "value": 1, "type": "Integer" },
480
+ "totalSpent": { "value": 10000, "type": "Double" }
481
+ }
482
+ }'
483
+ # → 1 beneficio: ENVIO_GRATIS_BASICO
484
+
485
+ # Test 3: Cliente Silver, 4 años, $80k → envío básico + aniversario + volumen
486
+ curl -s -X POST \
487
+ http://localhost:8090/engine-rest/decision-definition/key/determineBenefits/evaluate \
488
+ -H "Content-Type: application/json" \
489
+ -d '{
490
+ "variables": {
491
+ "membershipLevel": { "value": "SILVER", "type": "String" },
492
+ "memberYears": { "value": 4, "type": "Integer" },
493
+ "totalSpent": { "value": 80000, "type": "Double" }
494
+ }
495
+ }'
496
+ # → 3 beneficios: ENVIO_GRATIS_BASICO, DESCUENTO_ANIVERSARIO, DESCUENTO_VOLUMEN
497
+ ```
498
+
499
+ ---
500
+
501
+ ## 5. Ejemplo 3 — Precio dinámico con FEEL (UNIQUE)
502
+
503
+ **Escenario:** Un hotel calcula el precio por noche según la temporada, tipo de habitación y si el huésped es miembro del programa de lealtad. Se usa `UNIQUE` porque cada combinación tiene exactamente un precio — si hay ambigüedad es un error de diseño.
504
+
505
+ ### Archivo DMN
506
+
507
+ Crear `deployments/hotel-pricing.dmn`:
508
+
509
+ ```xml
510
+ <?xml version="1.0" encoding="UTF-8"?>
511
+ <definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
512
+ id="hotel-pricing" name="Hotel Dynamic Pricing"
513
+ namespace="http://camunda.org/schema/1.0/dmn">
514
+
515
+ <decision id="calculateRoomPrice" name="Calculate Room Price">
516
+ <decisionTable id="dt_pricing" hitPolicy="UNIQUE">
517
+
518
+ <!-- INPUTS -->
519
+ <input id="i_season" label="Season">
520
+ <inputExpression id="ie_season" typeRef="string">
521
+ <text>season</text>
522
+ </inputExpression>
523
+ <inputValues><text>"HIGH","MEDIUM","LOW"</text></inputValues>
524
+ </input>
525
+
526
+ <input id="i_room" label="Room Type">
527
+ <inputExpression id="ie_room" typeRef="string">
528
+ <text>roomType</text>
529
+ </inputExpression>
530
+ <inputValues><text>"STANDARD","DELUXE","SUITE"</text></inputValues>
531
+ </input>
532
+
533
+ <input id="i_loyalty" label="Loyalty Member">
534
+ <inputExpression id="ie_loyalty" typeRef="boolean">
535
+ <text>isLoyaltyMember</text>
536
+ </inputExpression>
537
+ </input>
538
+
539
+ <!-- OUTPUTS -->
540
+ <output id="o_price" label="Base Price/Night" name="basePrice" typeRef="double"/>
541
+ <output id="o_discount" label="Loyalty Discount %" name="loyaltyDiscount" typeRef="double"/>
542
+
543
+ <!-- HIGH SEASON -->
544
+ <rule id="r1">
545
+ <inputEntry><text>"HIGH"</text></inputEntry>
546
+ <inputEntry><text>"STANDARD"</text></inputEntry>
547
+ <inputEntry><text>true</text></inputEntry>
548
+ <outputEntry><text>2500</text></outputEntry>
549
+ <outputEntry><text>10</text></outputEntry>
550
+ </rule>
551
+ <rule id="r2">
552
+ <inputEntry><text>"HIGH"</text></inputEntry>
553
+ <inputEntry><text>"STANDARD"</text></inputEntry>
554
+ <inputEntry><text>false</text></inputEntry>
555
+ <outputEntry><text>2500</text></outputEntry>
556
+ <outputEntry><text>0</text></outputEntry>
557
+ </rule>
558
+ <rule id="r3">
559
+ <inputEntry><text>"HIGH"</text></inputEntry>
560
+ <inputEntry><text>"DELUXE"</text></inputEntry>
561
+ <inputEntry><text>true</text></inputEntry>
562
+ <outputEntry><text>4200</text></outputEntry>
563
+ <outputEntry><text>12</text></outputEntry>
564
+ </rule>
565
+ <rule id="r4">
566
+ <inputEntry><text>"HIGH"</text></inputEntry>
567
+ <inputEntry><text>"DELUXE"</text></inputEntry>
568
+ <inputEntry><text>false</text></inputEntry>
569
+ <outputEntry><text>4200</text></outputEntry>
570
+ <outputEntry><text>0</text></outputEntry>
571
+ </rule>
572
+ <rule id="r5">
573
+ <inputEntry><text>"HIGH"</text></inputEntry>
574
+ <inputEntry><text>"SUITE"</text></inputEntry>
575
+ <inputEntry><text>true</text></inputEntry>
576
+ <outputEntry><text>8500</text></outputEntry>
577
+ <outputEntry><text>15</text></outputEntry>
578
+ </rule>
579
+ <rule id="r6">
580
+ <inputEntry><text>"HIGH"</text></inputEntry>
581
+ <inputEntry><text>"SUITE"</text></inputEntry>
582
+ <inputEntry><text>false</text></inputEntry>
583
+ <outputEntry><text>8500</text></outputEntry>
584
+ <outputEntry><text>0</text></outputEntry>
585
+ </rule>
586
+
587
+ <!-- MEDIUM SEASON -->
588
+ <rule id="r7">
589
+ <inputEntry><text>"MEDIUM"</text></inputEntry>
590
+ <inputEntry><text>"STANDARD"</text></inputEntry>
591
+ <inputEntry><text>true</text></inputEntry>
592
+ <outputEntry><text>1800</text></outputEntry>
593
+ <outputEntry><text>10</text></outputEntry>
594
+ </rule>
595
+ <rule id="r8">
596
+ <inputEntry><text>"MEDIUM"</text></inputEntry>
597
+ <inputEntry><text>"STANDARD"</text></inputEntry>
598
+ <inputEntry><text>false</text></inputEntry>
599
+ <outputEntry><text>1800</text></outputEntry>
600
+ <outputEntry><text>0</text></outputEntry>
601
+ </rule>
602
+ <rule id="r9">
603
+ <inputEntry><text>"MEDIUM"</text></inputEntry>
604
+ <inputEntry><text>"DELUXE"</text></inputEntry>
605
+ <inputEntry><text>true</text></inputEntry>
606
+ <outputEntry><text>3000</text></outputEntry>
607
+ <outputEntry><text>12</text></outputEntry>
608
+ </rule>
609
+ <rule id="r10">
610
+ <inputEntry><text>"MEDIUM"</text></inputEntry>
611
+ <inputEntry><text>"DELUXE"</text></inputEntry>
612
+ <inputEntry><text>false</text></inputEntry>
613
+ <outputEntry><text>3000</text></outputEntry>
614
+ <outputEntry><text>0</text></outputEntry>
615
+ </rule>
616
+ <rule id="r11">
617
+ <inputEntry><text>"MEDIUM"</text></inputEntry>
618
+ <inputEntry><text>"SUITE"</text></inputEntry>
619
+ <inputEntry><text>true</text></inputEntry>
620
+ <outputEntry><text>6000</text></outputEntry>
621
+ <outputEntry><text>15</text></outputEntry>
622
+ </rule>
623
+ <rule id="r12">
624
+ <inputEntry><text>"MEDIUM"</text></inputEntry>
625
+ <inputEntry><text>"SUITE"</text></inputEntry>
626
+ <inputEntry><text>false</text></inputEntry>
627
+ <outputEntry><text>6000</text></outputEntry>
628
+ <outputEntry><text>0</text></outputEntry>
629
+ </rule>
630
+
631
+ <!-- LOW SEASON -->
632
+ <rule id="r13">
633
+ <inputEntry><text>"LOW"</text></inputEntry>
634
+ <inputEntry><text>"STANDARD"</text></inputEntry>
635
+ <inputEntry><text>true</text></inputEntry>
636
+ <outputEntry><text>1200</text></outputEntry>
637
+ <outputEntry><text>10</text></outputEntry>
638
+ </rule>
639
+ <rule id="r14">
640
+ <inputEntry><text>"LOW"</text></inputEntry>
641
+ <inputEntry><text>"STANDARD"</text></inputEntry>
642
+ <inputEntry><text>false</text></inputEntry>
643
+ <outputEntry><text>1200</text></outputEntry>
644
+ <outputEntry><text>0</text></outputEntry>
645
+ </rule>
646
+ <rule id="r15">
647
+ <inputEntry><text>"LOW"</text></inputEntry>
648
+ <inputEntry><text>"DELUXE"</text></inputEntry>
649
+ <inputEntry><text>true</text></inputEntry>
650
+ <outputEntry><text>2000</text></outputEntry>
651
+ <outputEntry><text>12</text></outputEntry>
652
+ </rule>
653
+ <rule id="r16">
654
+ <inputEntry><text>"LOW"</text></inputEntry>
655
+ <inputEntry><text>"DELUXE"</text></inputEntry>
656
+ <inputEntry><text>false</text></inputEntry>
657
+ <outputEntry><text>2000</text></outputEntry>
658
+ <outputEntry><text>0</text></outputEntry>
659
+ </rule>
660
+ <rule id="r17">
661
+ <inputEntry><text>"LOW"</text></inputEntry>
662
+ <inputEntry><text>"SUITE"</text></inputEntry>
663
+ <inputEntry><text>true</text></inputEntry>
664
+ <outputEntry><text>4000</text></outputEntry>
665
+ <outputEntry><text>15</text></outputEntry>
666
+ </rule>
667
+ <rule id="r18">
668
+ <inputEntry><text>"LOW"</text></inputEntry>
669
+ <inputEntry><text>"SUITE"</text></inputEntry>
670
+ <inputEntry><text>false</text></inputEntry>
671
+ <outputEntry><text>4000</text></outputEntry>
672
+ <outputEntry><text>0</text></outputEntry>
673
+ </rule>
674
+
675
+ </decisionTable>
676
+ </decision>
677
+ </definitions>
678
+ ```
679
+
680
+ ### Pruebas
681
+
682
+ ```bash
683
+ docker-compose restart camunda
684
+
685
+ # Suite en temporada alta, miembro → $8500 - 15% = $7225 (precio final)
686
+ curl -s -X POST \
687
+ http://localhost:8090/engine-rest/decision-definition/key/calculateRoomPrice/evaluate \
688
+ -H "Content-Type: application/json" \
689
+ -d '{
690
+ "variables": {
691
+ "season": { "value": "HIGH", "type": "String" },
692
+ "roomType": { "value": "SUITE", "type": "String" },
693
+ "isLoyaltyMember": { "value": true, "type": "Boolean" }
694
+ }
695
+ }'
696
+ # → [{"basePrice":{"value":8500.0},"loyaltyDiscount":{"value":15.0}}]
697
+
698
+ # Standard en temporada baja, no miembro → $1200, sin descuento
699
+ curl -s -X POST \
700
+ http://localhost:8090/engine-rest/decision-definition/key/calculateRoomPrice/evaluate \
701
+ -H "Content-Type: application/json" \
702
+ -d '{
703
+ "variables": {
704
+ "season": { "value": "LOW", "type": "String" },
705
+ "roomType": { "value": "STANDARD", "type": "String" },
706
+ "isLoyaltyMember": { "value": false, "type": "Boolean" }
707
+ }
708
+ }'
709
+ # → [{"basePrice":{"value":1200.0},"loyaltyDiscount":{"value":0.0}}]
710
+ ```
711
+
712
+ ---
713
+
714
+ ## 6. Ejemplo 4 — Cadena de decisiones DRG
715
+
716
+ **Escenario:** Un sistema de logística necesita dos decisiones encadenadas:
717
+ 1. **Clasificar el paquete** según peso y volumen → determina la categoría
718
+ 2. **Calcular el costo de envío** según la categoría (output de decisión 1) + distancia + urgencia
719
+
720
+ Camunda resuelve automáticamente la cadena: al evaluar la decisión 2, evalúa primero la decisión 1.
721
+
722
+ ```
723
+ ┌──────────────────┐ ┌───────────────────┐
724
+ │ Classify Package │────→│ Calculate Shipping │
725
+ │ (peso, volumen) │ │ (categoría + dist) │
726
+ └──────────────────┘ └───────────────────┘
727
+ ```
728
+
729
+ ### Archivo DMN
730
+
731
+ Crear `deployments/shipping-drg.dmn`:
732
+
733
+ ```xml
734
+ <?xml version="1.0" encoding="UTF-8"?>
735
+ <definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
736
+ id="shipping-drg" name="Shipping Decision Requirements Graph"
737
+ namespace="http://camunda.org/schema/1.0/dmn">
738
+
739
+ <!-- Decision 1: Classify Package -->
740
+ <decision id="classifyPackage" name="Classify Package">
741
+ <decisionTable id="dt_classify" hitPolicy="FIRST">
742
+
743
+ <input id="i_weight">
744
+ <inputExpression typeRef="double"><text>weightKg</text></inputExpression>
745
+ </input>
746
+ <input id="i_volume">
747
+ <inputExpression typeRef="double"><text>volumeCm3</text></inputExpression>
748
+ </input>
749
+
750
+ <output id="o_category" name="packageCategory" typeRef="string"/>
751
+ <output id="o_handling" name="specialHandling" typeRef="boolean"/>
752
+
753
+ <!-- Oversized: heavy OR very large volume -->
754
+ <rule id="c1">
755
+ <inputEntry><text>&gt; 30</text></inputEntry>
756
+ <inputEntry><text></text></inputEntry>
757
+ <outputEntry><text>"OVERSIZED"</text></outputEntry>
758
+ <outputEntry><text>true</text></outputEntry>
759
+ </rule>
760
+ <rule id="c2">
761
+ <inputEntry><text></text></inputEntry>
762
+ <inputEntry><text>&gt; 500000</text></inputEntry>
763
+ <outputEntry><text>"OVERSIZED"</text></outputEntry>
764
+ <outputEntry><text>true</text></outputEntry>
765
+ </rule>
766
+
767
+ <!-- Large -->
768
+ <rule id="c3">
769
+ <inputEntry><text>[10..30]</text></inputEntry>
770
+ <inputEntry><text></text></inputEntry>
771
+ <outputEntry><text>"LARGE"</text></outputEntry>
772
+ <outputEntry><text>false</text></outputEntry>
773
+ </rule>
774
+
775
+ <!-- Medium -->
776
+ <rule id="c4">
777
+ <inputEntry><text>[2..10)</text></inputEntry>
778
+ <inputEntry><text></text></inputEntry>
779
+ <outputEntry><text>"MEDIUM"</text></outputEntry>
780
+ <outputEntry><text>false</text></outputEntry>
781
+ </rule>
782
+
783
+ <!-- Small (default) -->
784
+ <rule id="c5">
785
+ <inputEntry><text></text></inputEntry>
786
+ <inputEntry><text></text></inputEntry>
787
+ <outputEntry><text>"SMALL"</text></outputEntry>
788
+ <outputEntry><text>false</text></outputEntry>
789
+ </rule>
790
+
791
+ </decisionTable>
792
+ </decision>
793
+
794
+ <!-- Decision 2: Calculate Shipping Cost (depends on classifyPackage) -->
795
+ <decision id="calculateShipping" name="Calculate Shipping Cost">
796
+ <informationRequirement>
797
+ <requiredDecision href="#classifyPackage"/>
798
+ </informationRequirement>
799
+
800
+ <decisionTable id="dt_shipping" hitPolicy="FIRST">
801
+
802
+ <input id="i_cat">
803
+ <inputExpression typeRef="string">
804
+ <text>classifyPackage.packageCategory</text>
805
+ </inputExpression>
806
+ </input>
807
+ <input id="i_dist">
808
+ <inputExpression typeRef="double"><text>distanceKm</text></inputExpression>
809
+ </input>
810
+ <input id="i_urgent">
811
+ <inputExpression typeRef="boolean"><text>isUrgent</text></inputExpression>
812
+ </input>
813
+
814
+ <output id="o_cost" name="shippingCost" typeRef="double"/>
815
+ <output id="o_days" name="estimatedDays" typeRef="integer"/>
816
+ <output id="o_carrier" name="carrier" typeRef="string"/>
817
+
818
+ <!-- OVERSIZED — siempre flete especial -->
819
+ <rule id="s1">
820
+ <inputEntry><text>"OVERSIZED"</text></inputEntry>
821
+ <inputEntry><text></text></inputEntry>
822
+ <inputEntry><text>true</text></inputEntry>
823
+ <outputEntry><text>850</text></outputEntry>
824
+ <outputEntry><text>2</text></outputEntry>
825
+ <outputEntry><text>"FLETE_EXPRESS"</text></outputEntry>
826
+ </rule>
827
+ <rule id="s2">
828
+ <inputEntry><text>"OVERSIZED"</text></inputEntry>
829
+ <inputEntry><text></text></inputEntry>
830
+ <inputEntry><text>false</text></inputEntry>
831
+ <outputEntry><text>500</text></outputEntry>
832
+ <outputEntry><text>5</text></outputEntry>
833
+ <outputEntry><text>"FLETE_STANDARD"</text></outputEntry>
834
+ </rule>
835
+
836
+ <!-- LARGE + larga distancia -->
837
+ <rule id="s3">
838
+ <inputEntry><text>"LARGE"</text></inputEntry>
839
+ <inputEntry><text>&gt; 500</text></inputEntry>
840
+ <inputEntry><text>true</text></inputEntry>
841
+ <outputEntry><text>350</text></outputEntry>
842
+ <outputEntry><text>2</text></outputEntry>
843
+ <outputEntry><text>"PAQUETERIA_EXPRESS"</text></outputEntry>
844
+ </rule>
845
+ <rule id="s4">
846
+ <inputEntry><text>"LARGE"</text></inputEntry>
847
+ <inputEntry><text>&gt; 500</text></inputEntry>
848
+ <inputEntry><text>false</text></inputEntry>
849
+ <outputEntry><text>200</text></outputEntry>
850
+ <outputEntry><text>5</text></outputEntry>
851
+ <outputEntry><text>"PAQUETERIA_STANDARD"</text></outputEntry>
852
+ </rule>
853
+ <rule id="s5">
854
+ <inputEntry><text>"LARGE"</text></inputEntry>
855
+ <inputEntry><text></text></inputEntry>
856
+ <inputEntry><text></text></inputEntry>
857
+ <outputEntry><text>150</text></outputEntry>
858
+ <outputEntry><text>3</text></outputEntry>
859
+ <outputEntry><text>"PAQUETERIA_STANDARD"</text></outputEntry>
860
+ </rule>
861
+
862
+ <!-- MEDIUM -->
863
+ <rule id="s6">
864
+ <inputEntry><text>"MEDIUM"</text></inputEntry>
865
+ <inputEntry><text></text></inputEntry>
866
+ <inputEntry><text>true</text></inputEntry>
867
+ <outputEntry><text>120</text></outputEntry>
868
+ <outputEntry><text>1</text></outputEntry>
869
+ <outputEntry><text>"COURIER_EXPRESS"</text></outputEntry>
870
+ </rule>
871
+ <rule id="s7">
872
+ <inputEntry><text>"MEDIUM"</text></inputEntry>
873
+ <inputEntry><text></text></inputEntry>
874
+ <inputEntry><text>false</text></inputEntry>
875
+ <outputEntry><text>75</text></outputEntry>
876
+ <outputEntry><text>3</text></outputEntry>
877
+ <outputEntry><text>"COURIER_STANDARD"</text></outputEntry>
878
+ </rule>
879
+
880
+ <!-- SMALL -->
881
+ <rule id="s8">
882
+ <inputEntry><text>"SMALL"</text></inputEntry>
883
+ <inputEntry><text></text></inputEntry>
884
+ <inputEntry><text>true</text></inputEntry>
885
+ <outputEntry><text>80</text></outputEntry>
886
+ <outputEntry><text>1</text></outputEntry>
887
+ <outputEntry><text>"COURIER_EXPRESS"</text></outputEntry>
888
+ </rule>
889
+ <rule id="s9">
890
+ <inputEntry><text>"SMALL"</text></inputEntry>
891
+ <inputEntry><text></text></inputEntry>
892
+ <inputEntry><text>false</text></inputEntry>
893
+ <outputEntry><text>45</text></outputEntry>
894
+ <outputEntry><text>4</text></outputEntry>
895
+ <outputEntry><text>"CORREO_POSTAL"</text></outputEntry>
896
+ </rule>
897
+
898
+ </decisionTable>
899
+ </decision>
900
+ </definitions>
901
+ ```
902
+
903
+ ### Pruebas
904
+
905
+ ```bash
906
+ docker-compose restart camunda
907
+
908
+ # Paquete pesado (35kg), 800km, urgente → classifies as OVERSIZED → FLETE_EXPRESS $850
909
+ curl -s -X POST \
910
+ http://localhost:8090/engine-rest/decision-definition/key/calculateShipping/evaluate \
911
+ -H "Content-Type: application/json" \
912
+ -d '{
913
+ "variables": {
914
+ "weightKg": { "value": 35, "type": "Double" },
915
+ "volumeCm3": { "value": 50000, "type": "Double" },
916
+ "distanceKm": { "value": 800, "type": "Double" },
917
+ "isUrgent": { "value": true, "type": "Boolean" }
918
+ }
919
+ }'
920
+ # → [{"shippingCost":{"value":850.0},"estimatedDays":{"value":2},"carrier":{"value":"FLETE_EXPRESS"}}]
921
+
922
+ # Paquete pequeño (0.5kg), 100km, no urgente → SMALL → CORREO_POSTAL $45
923
+ curl -s -X POST \
924
+ http://localhost:8090/engine-rest/decision-definition/key/calculateShipping/evaluate \
925
+ -H "Content-Type: application/json" \
926
+ -d '{
927
+ "variables": {
928
+ "weightKg": { "value": 0.5, "type": "Double" },
929
+ "volumeCm3": { "value": 3000, "type": "Double" },
930
+ "distanceKm": { "value": 100, "type": "Double" },
931
+ "isUrgent": { "value": false, "type": "Boolean" }
932
+ }
933
+ }'
934
+ # → [{"shippingCost":{"value":45.0},"estimatedDays":{"value":4},"carrier":{"value":"CORREO_POSTAL"}}]
935
+
936
+ # Paquete mediano (5kg), 300km, urgente → MEDIUM → COURIER_EXPRESS $120, 1 día
937
+ curl -s -X POST \
938
+ http://localhost:8090/engine-rest/decision-definition/key/calculateShipping/evaluate \
939
+ -H "Content-Type: application/json" \
940
+ -d '{
941
+ "variables": {
942
+ "weightKg": { "value": 5, "type": "Double" },
943
+ "volumeCm3": { "value": 30000, "type": "Double" },
944
+ "distanceKm": { "value": 300, "type": "Double" },
945
+ "isUrgent": { "value": true, "type": "Boolean" }
946
+ }
947
+ }'
948
+ # → [{"shippingCost":{"value":120.0},"estimatedDays":{"value":1},"carrier":{"value":"COURIER_EXPRESS"}}]
949
+ ```
950
+
951
+ ---
952
+
953
+ ## 7. Ejemplo 5 — Asignación de tickets (RULE ORDER)
954
+
955
+ **Escenario:** Un sistema de helpdesk asigna tickets de soporte según la urgencia, categoría del problema y horario. Se usa `RULE ORDER` porque se necesita una **lista priorizada** de equipos candidatos — el sistema intenta asignar al primero disponible, si no, al segundo, etc.
956
+
957
+ ### Archivo DMN
958
+
959
+ Crear `deployments/ticket-assignment.dmn`:
960
+
961
+ ```xml
962
+ <?xml version="1.0" encoding="UTF-8"?>
963
+ <definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
964
+ id="ticket-assignment" name="Ticket Assignment"
965
+ namespace="http://camunda.org/schema/1.0/dmn">
966
+
967
+ <decision id="assignTicket" name="Assign Support Ticket">
968
+ <decisionTable id="dt_assign" hitPolicy="RULE ORDER">
969
+
970
+ <!-- INPUTS -->
971
+ <input id="i_urgency">
972
+ <inputExpression typeRef="string"><text>urgency</text></inputExpression>
973
+ <inputValues><text>"CRITICAL","HIGH","MEDIUM","LOW"</text></inputValues>
974
+ </input>
975
+
976
+ <input id="i_category">
977
+ <inputExpression typeRef="string"><text>category</text></inputExpression>
978
+ <inputValues><text>"INFRASTRUCTURE","APPLICATION","SECURITY","GENERAL"</text></inputValues>
979
+ </input>
980
+
981
+ <input id="i_business_hours">
982
+ <inputExpression typeRef="boolean"><text>isBusinessHours</text></inputExpression>
983
+ </input>
984
+
985
+ <!-- OUTPUTS -->
986
+ <output id="o_team" name="assignedTeam" typeRef="string"/>
987
+ <output id="o_sla" name="slaHours" typeRef="integer"/>
988
+ <output id="o_escalation" name="escalationLevel" typeRef="string"/>
989
+
990
+ <!-- CRITICAL + Security → Security team first, then senior on-call -->
991
+ <rule id="r1">
992
+ <inputEntry><text>"CRITICAL"</text></inputEntry>
993
+ <inputEntry><text>"SECURITY"</text></inputEntry>
994
+ <inputEntry><text></text></inputEntry>
995
+ <outputEntry><text>"SECURITY_RESPONSE"</text></outputEntry>
996
+ <outputEntry><text>1</text></outputEntry>
997
+ <outputEntry><text>"VP_ENGINEERING"</text></outputEntry>
998
+ </rule>
999
+ <rule id="r2">
1000
+ <inputEntry><text>"CRITICAL"</text></inputEntry>
1001
+ <inputEntry><text>"SECURITY"</text></inputEntry>
1002
+ <inputEntry><text></text></inputEntry>
1003
+ <outputEntry><text>"SENIOR_ON_CALL"</text></outputEntry>
1004
+ <outputEntry><text>1</text></outputEntry>
1005
+ <outputEntry><text>"CTO"</text></outputEntry>
1006
+ </rule>
1007
+
1008
+ <!-- CRITICAL + Infrastructure → Infra team, then DevOps -->
1009
+ <rule id="r3">
1010
+ <inputEntry><text>"CRITICAL"</text></inputEntry>
1011
+ <inputEntry><text>"INFRASTRUCTURE"</text></inputEntry>
1012
+ <inputEntry><text></text></inputEntry>
1013
+ <outputEntry><text>"INFRASTRUCTURE_TEAM"</text></outputEntry>
1014
+ <outputEntry><text>2</text></outputEntry>
1015
+ <outputEntry><text>"ENGINEERING_MANAGER"</text></outputEntry>
1016
+ </rule>
1017
+ <rule id="r4">
1018
+ <inputEntry><text>"CRITICAL"</text></inputEntry>
1019
+ <inputEntry><text>"INFRASTRUCTURE"</text></inputEntry>
1020
+ <inputEntry><text></text></inputEntry>
1021
+ <outputEntry><text>"DEVOPS_ON_CALL"</text></outputEntry>
1022
+ <outputEntry><text>2</text></outputEntry>
1023
+ <outputEntry><text>"VP_ENGINEERING"</text></outputEntry>
1024
+ </rule>
1025
+
1026
+ <!-- HIGH during business hours → specialized team -->
1027
+ <rule id="r5">
1028
+ <inputEntry><text>"HIGH"</text></inputEntry>
1029
+ <inputEntry><text>"APPLICATION"</text></inputEntry>
1030
+ <inputEntry><text>true</text></inputEntry>
1031
+ <outputEntry><text>"APP_SUPPORT_L2"</text></outputEntry>
1032
+ <outputEntry><text>4</text></outputEntry>
1033
+ <outputEntry><text>"TEAM_LEAD"</text></outputEntry>
1034
+ </rule>
1035
+
1036
+ <!-- HIGH outside business hours → on-call -->
1037
+ <rule id="r6">
1038
+ <inputEntry><text>"HIGH"</text></inputEntry>
1039
+ <inputEntry><text></text></inputEntry>
1040
+ <inputEntry><text>false</text></inputEntry>
1041
+ <outputEntry><text>"ON_CALL_ROTATION"</text></outputEntry>
1042
+ <outputEntry><text>4</text></outputEntry>
1043
+ <outputEntry><text>"ENGINEERING_MANAGER"</text></outputEntry>
1044
+ </rule>
1045
+
1046
+ <!-- MEDIUM → L1 support -->
1047
+ <rule id="r7">
1048
+ <inputEntry><text>"MEDIUM"</text></inputEntry>
1049
+ <inputEntry><text></text></inputEntry>
1050
+ <inputEntry><text></text></inputEntry>
1051
+ <outputEntry><text>"SUPPORT_L1"</text></outputEntry>
1052
+ <outputEntry><text>8</text></outputEntry>
1053
+ <outputEntry><text>"TEAM_LEAD"</text></outputEntry>
1054
+ </rule>
1055
+
1056
+ <!-- LOW → general queue -->
1057
+ <rule id="r8">
1058
+ <inputEntry><text>"LOW"</text></inputEntry>
1059
+ <inputEntry><text></text></inputEntry>
1060
+ <inputEntry><text></text></inputEntry>
1061
+ <outputEntry><text>"GENERAL_QUEUE"</text></outputEntry>
1062
+ <outputEntry><text>24</text></outputEntry>
1063
+ <outputEntry><text>"NONE"</text></outputEntry>
1064
+ </rule>
1065
+
1066
+ </decisionTable>
1067
+ </decision>
1068
+ </definitions>
1069
+ ```
1070
+
1071
+ ### Pruebas
1072
+
1073
+ ```bash
1074
+ docker-compose restart camunda
1075
+
1076
+ # Incidente crítico de seguridad → 2 candidatos en orden de prioridad
1077
+ curl -s -X POST \
1078
+ http://localhost:8090/engine-rest/decision-definition/key/assignTicket/evaluate \
1079
+ -H "Content-Type: application/json" \
1080
+ -d '{
1081
+ "variables": {
1082
+ "urgency": { "value": "CRITICAL", "type": "String" },
1083
+ "category": { "value": "SECURITY", "type": "String" },
1084
+ "isBusinessHours": { "value": false, "type": "Boolean" }
1085
+ }
1086
+ }'
1087
+ # → [
1088
+ # {"assignedTeam":"SECURITY_RESPONSE","slaHours":1,"escalationLevel":"VP_ENGINEERING"},
1089
+ # {"assignedTeam":"SENIOR_ON_CALL","slaHours":1,"escalationLevel":"CTO"}
1090
+ # ]
1091
+
1092
+ # Problema medio general → L1, SLA 8h
1093
+ curl -s -X POST \
1094
+ http://localhost:8090/engine-rest/decision-definition/key/assignTicket/evaluate \
1095
+ -H "Content-Type: application/json" \
1096
+ -d '{
1097
+ "variables": {
1098
+ "urgency": { "value": "MEDIUM", "type": "String" },
1099
+ "category": { "value": "GENERAL", "type": "String" },
1100
+ "isBusinessHours": { "value": true, "type": "Boolean" }
1101
+ }
1102
+ }'
1103
+ # → [{"assignedTeam":"SUPPORT_L1","slaHours":8,"escalationLevel":"TEAM_LEAD"}]
1104
+
1105
+ # Ticket bajo → cola general, SLA 24h
1106
+ curl -s -X POST \
1107
+ http://localhost:8090/engine-rest/decision-definition/key/assignTicket/evaluate \
1108
+ -H "Content-Type: application/json" \
1109
+ -d '{
1110
+ "variables": {
1111
+ "urgency": { "value": "LOW", "type": "String" },
1112
+ "category": { "value": "APPLICATION", "type": "String" },
1113
+ "isBusinessHours": { "value": true, "type": "Boolean" }
1114
+ }
1115
+ }'
1116
+ # → [{"assignedTeam":"GENERAL_QUEUE","slaHours":24,"escalationLevel":"NONE"}]
1117
+ ```
1118
+
1119
+ ---
1120
+
1121
+ ## 8. API REST de Camunda — Referencia completa
1122
+
1123
+ ### Decisiones
1124
+
1125
+ ```bash
1126
+ # Listar todas las decisiones desplegadas
1127
+ curl -s http://localhost:8090/engine-rest/decision-definition | python -m json.tool
1128
+
1129
+ # Obtener una decisión por key
1130
+ curl -s http://localhost:8090/engine-rest/decision-definition/key/evaluateCredit
1131
+
1132
+ # Obtener el XML (.dmn) de una decisión
1133
+ curl -s http://localhost:8090/engine-rest/decision-definition/key/evaluateCredit/xml
1134
+
1135
+ # Evaluar una decisión
1136
+ curl -s -X POST \
1137
+ http://localhost:8090/engine-rest/decision-definition/key/{decisionKey}/evaluate \
1138
+ -H "Content-Type: application/json" \
1139
+ -d '{ "variables": { ... } }'
1140
+
1141
+ # Listar versiones de una decisión
1142
+ curl -s "http://localhost:8090/engine-rest/decision-definition?key=evaluateCredit&sortBy=version&sortOrder=desc"
1143
+ ```
1144
+
1145
+ ### Deployments
1146
+
1147
+ ```bash
1148
+ # Listar todos los deployments
1149
+ curl -s http://localhost:8090/engine-rest/deployment | python -m json.tool
1150
+
1151
+ # Deploy programático (sin restart del container)
1152
+ curl -X POST http://localhost:8090/engine-rest/deployment/create \
1153
+ -F "deployment-name=credit-rules-v2" \
1154
+ -F "deploy-changed-only=true" \
1155
+ -F "data=@deployments/credit-approval.dmn"
1156
+
1157
+ # Eliminar un deployment
1158
+ curl -X DELETE "http://localhost:8090/engine-rest/deployment/{deploymentId}?cascade=true"
1159
+ ```
1160
+
1161
+ ### Historial
1162
+
1163
+ ```bash
1164
+ # Historial de evaluaciones de una decisión
1165
+ curl -s "http://localhost:8090/engine-rest/history/decision-instance?decisionDefinitionKey=evaluateCredit" \
1166
+ | python -m json.tool
1167
+
1168
+ # Detalle de una evaluación (inputs y outputs)
1169
+ curl -s "http://localhost:8090/engine-rest/history/decision-instance/{instanceId}" \
1170
+ | python -m json.tool
1171
+ ```
1172
+
1173
+ ### Formato de variables
1174
+
1175
+ Estructura estándar para enviar variables a Camunda:
1176
+
1177
+ ```json
1178
+ {
1179
+ "variables": {
1180
+ "stringVar": { "value": "HELLO", "type": "String" },
1181
+ "intVar": { "value": 42, "type": "Integer" },
1182
+ "longVar": { "value": 100000000, "type": "Long" },
1183
+ "doubleVar": { "value": 3.14, "type": "Double" },
1184
+ "boolVar": { "value": true, "type": "Boolean" },
1185
+ "dateVar": { "value": "2026-04-02T10:00:00.000-0500", "type": "Date" }
1186
+ }
1187
+ }
1188
+ ```
1189
+
1190
+ ---
1191
+
1192
+ ## 9. Consumir desde Spring Boot (sin eva4j)
1193
+
1194
+ ### Dependencias (build.gradle)
1195
+
1196
+ ```groovy
1197
+ dependencies {
1198
+ implementation 'org.springframework.boot:spring-boot-starter-web'
1199
+ implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
1200
+ }
1201
+ ```
1202
+
1203
+ ### Puerto de dominio
1204
+
1205
+ ```java
1206
+ // domain/repositories/CreditEvaluator.java
1207
+ public interface CreditEvaluator {
1208
+ CreditDecision evaluate(int creditScore, double monthlyIncome, double requestedAmount);
1209
+ }
1210
+
1211
+ // domain/models/CreditDecision.java
1212
+ public record CreditDecision(
1213
+ String decision,
1214
+ Double interestRate,
1215
+ boolean requiresReview
1216
+ ) {}
1217
+ ```
1218
+
1219
+ ### Feign Client a la REST API de Camunda
1220
+
1221
+ ```java
1222
+ // infrastructure/adapters/camunda/CamundaDmnClient.java
1223
+ @FeignClient(name = "camunda-dmn", url = "${camunda.engine.base-url}")
1224
+ public interface CamundaDmnClient {
1225
+
1226
+ @PostMapping("/engine-rest/decision-definition/key/{decisionKey}/evaluate")
1227
+ List<Map<String, CamundaVariable>> evaluate(
1228
+ @PathVariable String decisionKey,
1229
+ @RequestBody CamundaEvaluateRequest request
1230
+ );
1231
+ }
1232
+
1233
+ // infrastructure/adapters/camunda/CamundaVariable.java
1234
+ public record CamundaVariable(String type, Object value) {}
1235
+
1236
+ // infrastructure/adapters/camunda/CamundaEvaluateRequest.java
1237
+ public record CamundaEvaluateRequest(Map<String, CamundaVariable> variables) {}
1238
+ ```
1239
+
1240
+ ### Adapter (ACL)
1241
+
1242
+ ```java
1243
+ // infrastructure/adapters/camunda/CamundaCreditEvaluator.java
1244
+ @Component
1245
+ public class CamundaCreditEvaluator implements CreditEvaluator {
1246
+
1247
+ private final CamundaDmnClient dmnClient;
1248
+
1249
+ public CamundaCreditEvaluator(CamundaDmnClient dmnClient) {
1250
+ this.dmnClient = dmnClient;
1251
+ }
1252
+
1253
+ @Override
1254
+ public CreditDecision evaluate(int creditScore, double monthlyIncome,
1255
+ double requestedAmount) {
1256
+ var variables = Map.of(
1257
+ "creditScore", new CamundaVariable("Integer", creditScore),
1258
+ "monthlyIncome", new CamundaVariable("Double", monthlyIncome),
1259
+ "requestedAmount", new CamundaVariable("Double", requestedAmount)
1260
+ );
1261
+
1262
+ var results = dmnClient.evaluate("evaluateCredit",
1263
+ new CamundaEvaluateRequest(variables));
1264
+
1265
+ var result = results.get(0);
1266
+
1267
+ return new CreditDecision(
1268
+ (String) result.get("decision").value(),
1269
+ result.containsKey("interestRate")
1270
+ ? (Double) result.get("interestRate").value()
1271
+ : null,
1272
+ (Boolean) result.get("requiresReview").value()
1273
+ );
1274
+ }
1275
+ }
1276
+ ```
1277
+
1278
+ ### Use Case
1279
+
1280
+ ```java
1281
+ // application/usecases/ProcessCreditApplicationHandler.java
1282
+ @Component
1283
+ public class ProcessCreditApplicationHandler {
1284
+
1285
+ private final CreditEvaluator creditEvaluator;
1286
+ private final ApplicationRepository applicationRepository;
1287
+
1288
+ public ProcessCreditApplicationHandler(CreditEvaluator creditEvaluator,
1289
+ ApplicationRepository applicationRepository) {
1290
+ this.creditEvaluator = creditEvaluator;
1291
+ this.applicationRepository = applicationRepository;
1292
+ }
1293
+
1294
+ public void handle(ProcessCreditApplicationCommand command) {
1295
+ CreditApplication app = applicationRepository.findById(command.applicationId())
1296
+ .orElseThrow();
1297
+
1298
+ // Delega la decisión al motor de reglas
1299
+ CreditDecision decision = creditEvaluator.evaluate(
1300
+ app.getCreditScore(),
1301
+ app.getMonthlyIncome(),
1302
+ app.getRequestedAmount()
1303
+ );
1304
+
1305
+ app.applyDecision(decision);
1306
+ applicationRepository.save(app);
1307
+ }
1308
+ }
1309
+ ```
1310
+
1311
+ ### Configuración
1312
+
1313
+ ```yaml
1314
+ # application.yml
1315
+ camunda:
1316
+ engine:
1317
+ base-url: http://localhost:8090
1318
+ ```
1319
+
1320
+ ---
1321
+
1322
+ ## 10. Integración futura con eva4j
1323
+
1324
+ ### Lo que ya funciona hoy con `ports[]`
1325
+
1326
+ El lado consumidor (microservicio → Camunda) se puede declarar en `domain.yaml`:
1327
+
1328
+ ```yaml
1329
+ ports:
1330
+ - name: evaluateCredit
1331
+ service: CreditRuleEngine
1332
+ target: camunda-engine-external
1333
+ baseUrl: http://localhost:8090
1334
+ http: POST /engine-rest/decision-definition/key/evaluateCredit/evaluate
1335
+ body:
1336
+ - name: creditScore
1337
+ type: Integer
1338
+ - name: monthlyIncome
1339
+ type: BigDecimal
1340
+ - name: requestedAmount
1341
+ type: BigDecimal
1342
+ fields:
1343
+ - name: decision
1344
+ type: String
1345
+ - name: interestRate
1346
+ type: BigDecimal
1347
+ - name: requiresReview
1348
+ type: Boolean
1349
+ ```
1350
+
1351
+ Esto genera automáticamente: puerto de dominio, FeignClient, FeignAdapter con ACL, domain model, DTOs de infraestructura.
1352
+
1353
+ ### Lo que se podría agregar
1354
+
1355
+ ```bash
1356
+ # Capacidad DMN nativa
1357
+ eva add dmn-client
1358
+
1359
+ # Generar regla + consumidor en un solo comando
1360
+ eva g dmn-rule credits EvaluateCredit
1361
+ ```
1362
+
1363
+ Generaría: archivo `.dmn` scaffold, puerto de dominio, Feign/gRPC client, ACL mapper, script de deploy a Camunda.
1364
+
1365
+ ---
1366
+
1367
+ ## Resumen del learning path
1368
+
1369
+ | Fase | Contenido | Tipo |
1370
+ |------|-----------|------|
1371
+ | 1 | Docker Compose | Infraestructura |
1372
+ | 2 | Conceptos DMN (hit policies, FEEL, tipos) | Teoría |
1373
+ | 3 | Aprobación de crédito (FIRST) | Hands-on |
1374
+ | 4 | Beneficios de membresía (COLLECT) | Hands-on |
1375
+ | 5 | Precio dinámico hotel (UNIQUE) | Hands-on |
1376
+ | 6 | Cadena de decisiones logística (DRG) | Hands-on |
1377
+ | 7 | Asignación de tickets (RULE ORDER) | Hands-on |
1378
+ | 8 | API REST de Camunda | Referencia |
1379
+ | 9 | Consumir desde Spring Boot | Integración |
1380
+ | 10 | Integración con eva4j (ports[]) | Futuro |