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.
- package/AGENTS.md +2 -0
- package/DOMAIN_YAML_GUIDE.md +3 -1
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +272 -8
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-read-models.yaml +2 -2
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +31 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +308 -16
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-temporal-activity.js +968 -34
- package/src/commands/generate-temporal-flow.js +95 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/skills/build-system-yaml/SKILL.md +222 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +50 -4
- package/src/skills/build-system-yaml/references/module-spec.md +57 -8
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/domain-validator.js +266 -4
- package/src/utils/naming.js +10 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/yaml-to-entity.js +10 -7
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelJpa.java.ejs +1 -1
- package/templates/read-model/ReadModelJpaRepository.java.ejs +2 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +9 -5
- package/templates/read-model/ReadModelSyncHandler.java.ejs +2 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- package/COMMAND_EVALUATION.md +0 -911
- package/test-c2010.js +0 -49
- package/test-update-compat.js +0 -109
- 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>< 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>> 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>>= 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>> 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>>= 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>>= 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>>= 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>>= 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>> 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>> 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>> 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>> 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 |
|