# Interoperabilidad Multi-Agente con AfexFields
## Resumen
El sistema de *afexFields* permite que una transferencia sea procesada por múltiples agentes mediante transformación dinámica de campos. Cada agente puede tener sus propios formatos y requisitos específicos.
Como concepto, cuando hablamos de **afexFields** nos referimos al uso de los campos `afexId` y `afexValue` en las configuraciones de los agentes, que permiten mapear los valores estándar de AFEX a los formatos específicos requeridos por cada agente.
## Componentes
- **TransferAPI** - Maneja transferencias y solicita transformaciones
- **AgentAPI** - Especializado en transformar campos por agente
- **AgentFieldsService** - Servicio contenido en **AgentAPI** responsable de ejecutar la lógica de transformación
### Flujo de Datos
```
Transfer API → Agent API → AgentFieldsService → Transformación → Respuesta
```
---
## Configuración de Campos
Cada agente define sus propias configuraciones, basadas en cada país y método de pago. Estas configuraciones se almacenan en la tabla `agent-fields-[STAGE]` de cada ambiente en *DynamoDB* y es poblada mediante el *seeds* (actualmente en `agent-fields.json` en proyecto **transfer-api**).
```typescript
{
id: "agentId-countryId-methodPaymentId",
fields: [
{
id: "receiverBankCode", // ID del campo para el agente
afexId: "receiverProviderCode", // ID estándar de AFEX
type: "select",
options: [
{
value: "001", // Valor esperado por el agente
afexValue: "BANK_XYZ", // Valor estándar de AFEX
label: "Banco XYZ"
}
]
}
]
}
```
## Transformación de Campos
### Conceptos Clave
**`afexId`**: Identificador estándar usado internamente por AFEX
**`afexValue`**: Valor estándar usado internamente por AFEX
**`id`**: Identificador específico requerido por el agente
**`value`**: Valor específico esperado por el agente
### Proceso de Transformación
El proceso de transformación es el **núcleo del sistema de interoperabilidad**. Convierte automáticamente los datos desde el formato estándar de AFEX hacia el formato específico que requiere cada agente.
#### ¿Por qué es necesaria la transformación?
**Problema:** Cada agente financiero tiene sus propios códigos y formatos:
- **Agente A** espera banco "001" para Banco XYZ
- **Agente B** espera banco "BXY" para el mismo Banco XYZ
- **AFEX** internamente usa "BANK_XYZ" como identificador estándar
**Solución:** El transformador actúa como traductor universal.
#### Flujo Completo de Transformación
```mermaid
flowchart TD
A[Usuario selecciona: Banco XYZ] --> B[Frontend envía: afexValue = BANK_XYZ]
B --> C[TransferAPI recibe agentFields]
C --> D[TransferAPI solicita transformación]
D --> E[AgentFieldsService ejecuta transformación]
E --> F{¿Configuración existe para agente?}
F -->|SÍ| G[Buscar campo por afexId]
F -->|NO| H[Usar valores originales]
G --> I{¿Campo encontrado?}
I -->|SÍ| J{¿Es tipo select?}
I -->|NO| K[Buscar por ID directo - Fallback]
J -->|SÍ| L[Buscar opción por afexValue]
J -->|NO| M[Usar valor directo]
L --> N{¿Opción encontrada?}
N -->|SÍ| O[Mapear: afexValue → value del agente]
N -->|NO| P[Buscar opción por value]
P --> Q{¿Opción encontrada?}
Q -->|SÍ| R[Mapear: value → value del agente]
Q -->|NO| S[Valor inválido - Error]
K --> T{¿Encontrado por ID?}
T -->|SÍ| U[Usar valor original]
T -->|NO| V[Campo no configurado - Preservar]
O --> W[Agente recibe: 001]
R --> W
M --> W
U --> W
H --> W
V --> W
style A fill:#e3f2fd
style W fill:#c8e6c9
style S fill:#ffcdd2
style E fill:#fff3e0
```
#### Pasos Detallados del Proceso
**1. Recepción de Datos**
```typescript
{
agentFields: {
"receiverProviderCode": "BANK_XYZ", // Valor estándar AFEX
"receiverProviderCode_label": "Banco XYZ", // Label para UI
"receiverAccountNumber": "123456789" // Campo de texto
}
}
```
**2. Búsqueda de Configuración**
El sistema busca la configuración específica usando la clave:
```
"agentId-countryId-methodPaymentId"
// Ejemplo: "EY-CO-1" (Terrapay, Colombia, Depósito)
```
**3. Transformación Campo por Campo**
Para cada campo en `agentFields`:
```typescript
// Campo: receiverProviderCode = "BANK_XYZ"
// PASO A: Buscar por afexId
configuracion = {
id: "receiverBankCode", // ← ID que espera el agente
afexId: "receiverProviderCode", // ← ID estándar de AFEX
type: "select",
options: [
{
value: "001",
afexValue: "BANK_XYZ" // ← Mapeo de valores
}
]
}
// PASO B: Mapear valor
"BANK_XYZ" (afexValue) → "001" (value del agente)
// PASO C: Resultado
{
id: "receiverBankCode", // ID transformado
value: "001" // Valor transformado
}
```
**4. Resultado Final**
```typescript
// Antes de transformación (formato AFEX)
{
"receiverProviderCode": "BANK_XYZ",
"receiverAccountNumber": "123456789"
}
// Después de transformación (formato Agente)
{
"receiverBankCode": "001", // ← Transformado
"receiverAccountNumber": "123456789" // ← Sin cambios (no configurado)
}
```
#### Casos Especiales
**Caso 1: Campo sin configuración (Retrocompatibilidad)**
```typescript
// Si el campo no existe en la configuración del agente
// Se preserva exactamente como llegó
{
"customField": "originalValue" // ← Permanece igual
}
```
**Caso 2: Transformación fallida**
```typescript
// Si afexValue no existe en las opciones del agente
// El campo se omite del resultado (undefined)
{
"receiverProviderCode": "INVALID_BANK" // ← No se incluye en resultado
}
```
**Caso 3: Campos de texto**
```typescript
// Para campos tipo "input", solo se cambia el ID
{
id: "accountNumber",
afexId: "receiverAccountNumber",
type: "input"
}
// "receiverAccountNumber": "123" → "accountNumber": "123"
```
### Retrocompatibilidad e implementación gradual
Actualmente el sistema realiza la transformación de cada campo en los valores de *agentFields* basado en el proceso descrito con anterioridad, asegurando que los datos se ajusten a las expectativas de cada agente.
Esta lógica convierte la transformación en un proceso **totalmente retrocompatible** con el modo utilizado actualmente al guardar/consultar los *agentFields* en las transferencias, retornando el mismo campo que se envió originalmente si no se encuentra en la configuración del agente.
De este modo, se asegura que la transformación no afecte a los agentes que aún no utilizan el sistema de *afexFields* para sus *agentFields*, permitiendo a cada uno de los flujos que los utilizan poder hacer una migración gradual hacia esta nueva configuración bajo su propia necesidad o disponibilidad, por cada campo e incluso por cada agente.
### Lógica de Transformación
#### Diagrama de Flujo
```mermaid
flowchart TD
A[Entrada: Campo AFEX] --> B{¿Existe campo con afexId?}
B -->|SÍ| C{¿Es tipo select?}
B -->|NO| D[Buscar por id directo - Fallback]
C -->|SÍ| E[Buscar opción por afexValue]
C -->|NO| F[Usar valor directo]
E --> G{¿Opción encontrada?}
G -->|SÍ| H[Mapear: afexValue → value]
G -->|NO| I[Buscar opción por value]
I --> J{¿Opción encontrada?}
J -->|SÍ| K[Mapear: value → value]
J -->|NO| L[Campo nulo - Valor inválido]
D --> M{¿Encontrado?}
M -->|SÍ| N[Usar valor original]
M -->|NO| L
F --> O[Campo transformado]
H --> O
K --> O
N --> O
L --> P[Error: Campo no encontrado]
style A fill:#e1f5fe
style O fill:#c8e6c9
style P fill:#ffcdd2
style B fill:#fff3e0
style C fill:#fff3e0
style G fill:#fff3e0
style J fill:#fff3e0
style M fill:#fff3e0
```
#### Algoritmo en Pseudocódigo
```
FUNCIÓN transformarCampo(campoAFEX, configuracionAgente):
// PASO 1: Buscar configuración por afexId
campo = buscarPor(configuracionAgente, "afexId", campoAFEX.id)
SI campo existe ENTONCES:
SI campo.tipo == "select" ENTONCES:
// PASO 2: Mapear valor para campos de selección por afexValue
opcion = buscarPor(campo.opciones, "afexValue", campoAFEX.afexValue)
SI opcion existe ENTONCES:
RETORNAR { id: campo.id, value: opcion.value }
SINO:
// PASO 3: Mapear valor para campos de selección por value
opcion = buscarPor(campo.opciones, "value", campoAFEX.value)
SI opcion existe ENTONCES:
RETORNAR { id: campo.id, value: opcion.value }
SINO:
RETORNAR null // Valor no válido
FIN SI
FIN SI
SINO:
// PASO 4: Usar valor directo para campos de texto
RETORNAR { id: campo.id, value: campoAFEX.value }
FIN SI
SINO:
// PASO 5: Fallback - buscar por id directo (retrocompatibilidad)
campoFallback = buscarPor(configuracionAgente, "id", campoAFEX.id)
SI campoFallback existe ENTONCES:
RETORNAR { id: campoFallback.id, value: campoAFEX.value }
SINO:
RETORNAR null // Campo no encontrado
FIN SI
FIN SI
FIN FUNCIÓN
```
#### Ejemplo Paso a Paso
**Configuración del Agente:**
```typescript
{
fields: [
{
id: "receiverBankCode",
afexId: "receiverProviderCode",
type: "select",
options: [
{
value: "001",
afexValue: "BANK_XYZ",
label: "Banco XYZ"
},
{
value: "002",
afexValue: "BANK_ABC",
label: "Banco ABC"
}
]
}
]
}
```
**Transformación:**
```
1. Entrada:
{
id: "receiverProviderCode",
value: "BANK_XYZ"
}
2. Buscar por afexId: "receiverProviderCode" ✓ Encontrado
3. ¿Es tipo select? ✓ SÍ
4. Buscar opción con afexValue: "BANK_XYZ" ✓ Encontrado
5. Resultado:
{
id: "receiverBankCode",
value: "001"
}
```
#### Manejo de Labels
En el proceso de obtención de *agentFields* algunos campos obtienen el valor de `label` que luego se inyecta internamente con el sufijo `_label` en la lista a modo de ser utilizado posteriormente como un valor de texto en la presentación de datos, ya sea en formularios o en los propios *payloads* para realizar el envío hacia los agentes. Esto también fue considerado en el transformador y se incluye un mecanismo especial para preservar y transformar *labels* asociados a los campos mediante los *afexFields*, sin dejar fuera la retrocompatibilidad descrita anteriormente para campos que aún no hacen uso de ellos.
Es por este motivo que los campos con el sufijo `_label` no son procesados directamente como los otros campos y son utilizados como complementos de ellos en su procesamiento.
**Convención de Labels:**
- Los *labels* se almacenan con el sufijo `_label`
- Ejemplo: `receiverBankCode_label` contiene el texto descriptivo de `receiverBankCode`
**Proceso de Transformación de Labels:**
```mermaid
flowchart TD
A[agentFieldsValues: receiverProviderCode] --> B[Intentar transformar campo]
B --> C{¿Transformación exitosa?}
C -->|SÍ| D[Usar campo transformado: receiverBankCode]
C -->|NO| E[Usar campo original: receiverProviderCode]
D --> H{¿Existe receiverProviderCode_label?}
E --> I{¿Existe receiverProviderCode_label?}
H -->|SÍ| J[receiverBankCode_label = valor original]
H -->|NO| K[Sin label]
I -->|SÍ| L[receiverProviderCode_label = valor original]
I -->|NO| M[Sin label]
J --> N[Resultado: Campo transformado + Label]
K --> O[Resultado: Solo campo transformado]
L --> P[Resultado: Campo original + Label]
M --> Q[Resultado: Solo campo original]
style A fill:#e1f5fe
style N fill:#c8e6c9
style O fill:#c8e6c9
style P fill:#fff3e0
style Q fill:#fff3e0
```
**Algoritmo de Labels:**
```
PARA cada campo en agentFieldsValues:
SI campo NO termina en "_label" ENTONCES:
// 1. Intentar transformar el campo principal
campoTransformado = transformarCampo(campo, configuracion)
// 2. Buscar si existe el label asociado en datos originales
labelOriginal = agentFieldsValues[campo.id + "_label"]
SI campoTransformado !== undefined ENTONCES:
// TRANSFORMACIÓN EXITOSA
resultado[campoTransformado.id] = campoTransformado.value
SI labelOriginal existe ENTONCES:
// Preservar label con el NUEVO ID transformado
resultado[campoTransformado.id + "_label"] = labelOriginal
FIN SI
SINO:
// TRANSFORMACIÓN FALLIDA - Usar campo original
resultado[campo.id] = campo.value
SI labelOriginal existe ENTONCES:
// Preservar label con el ID ORIGINAL
resultado[campo.id + "_label"] = labelOriginal
FIN SI
FIN SI
FIN SI
// Los campos "_label" se procesan automáticamente
// junto con su campo principal, por eso se saltan aquí
FIN PARA
```
**Ejemplo Práctico:**
```typescript
// Entrada
{
"receiverProviderCode": "BANK_XYZ",
"receiverProviderCode_label": "Banco XYZ - Sucursal Centro"
}
// Configuración del agente
{
id: "receiverBankCode",
afexId: "receiverProviderCode",
type: "select",
options: [
{
value: "001",
afexValue: "BANK_XYZ",
label: "Banco XYZ"
}
]
}
// Salida transformada
{
"receiverBankCode": "001",
"receiverBankCode_label": "Banco XYZ - Sucursal Centro" // Label preservado
}
```
**Beneficios del Sistema de Labels:**
- **Preservación de contexto**: Los labels descriptivos se mantienen tras la transformación
- **Experiencia de usuario**: Las interfaces pueden mostrar texto legible en lugar de códigos
- **Trazabilidad**: Se conserva la información original seleccionada por el usuario
- **Flexibilidad**: Los labels pueden contener información adicional específica del contexto
---
## Invocación del Servicio de Transformación
### Endpoint de la Agent API
El servicio de transformación se expone a través de un endpoint REST en la **Agent API**:
```
POST /v1/agents/:id/fields/transform
```
Donde `:id` corresponde al **ID del agente** que realizará la transformación.
Considerar que este servicio permite el envío de lotes de transferencias para optimizar las invocaciones del recurso al momento de solicitar las transformaciones.
### Estructura de la Petición
**URL de ejemplo:**
```
POST /v1/agents/EY/fields/transform
```
**Payload:**
```typescript
{
transfers: [
{
id: "transfer_123",
countryId: "CO",
methodPaymentId: 1,
agentFieldsValues: {
"receiverProviderCode": "BANK_XYZ",
"receiverProviderCode_label": "Banco XYZ",
"receiverAccountNumber": "123456789"
}
},
{
id: "transfer_456",
countryId: "CO",
methodPaymentId: 1,
agentFieldsValues: {
"receiverProviderCode": "BANK_ABC",
"receiverAccountNumber": "987654321"
}
}
]
}
```
### Respuesta del Servicio
```typescript
{
"success": true,
"data": [
{
"id": "transfer_123",
"agentFieldsValues": {
"receiverBankCode": "001",
"receiverBankCode_label": "Banco XYZ",
"receiverAccountNumber": "123456789"
}
},
{
"id": "transfer_456",
"agentFieldsValues": {
"receiverBankCode": "002",
"receiverAccountNumber": "987654321"
}
}
]
}
```
### Casos de Uso del Endpoint
1. **Creación de transferencia**: Transforma campos antes de guardar al Core
2. **Consulta de pendientes**: Adapta campos almacenados al formato del agente
3. **Validación previa**: Verifica que los campos sean válidos para el agente
4. **Migración de datos**: Convierte transferencias existentes al nuevo formato
---
## Casos de Uso
### Caso 1: Campo de Selección Multi-Agente
**Agente A** espera:
- Campo: `bankType`
- Valores: `COMMERCIAL`, `SAVINGS`
**Agente B** espera:
- Campo: `institutionType`
- Valores: `BANK`, `CREDIT_UNION`
**Configuración unificada:**
```typescript
// Para Agente A
{
id: "bankType",
afexId: "receiverAccountType",
options: [
{
value: "COMMERCIAL",
afexValue: "CHECKING",
label: "Cuenta Corriente"
},
{
value: "SAVINGS",
afexValue: "SAVINGS",
label: "Cuenta de Ahorros"
}
]
}
// Para Agente B
{
id: "institutionType",
afexId: "receiverAccountType",
options: [
{
value: "BANK",
afexValue: "CHECKING",
label: "Banco"
},
{
value: "CREDIT_UNION",
afexValue: "SAVINGS",
label: "Cooperativa"
}
]
}
```
**Ejemplos de Transformación:**
**Escenario 1: Usuario selecciona "Cuenta Corriente"**
```typescript
// Entrada (formato AFEX estándar)
{
agentFields: {
"receiverAccountType": "CHECKING",
"receiverAccountType_label": "Cuenta Corriente"
}
}
// Transformación para Agente A
{
"bankType": "COMMERCIAL",
"bankType_label": "Cuenta Corriente"
}
// Transformación para Agente B
{
"institutionType": "BANK",
"institutionType_label": "Cuenta Corriente"
}
```
**Escenario 2: Usuario selecciona "Cuenta de Ahorros"**
```typescript
// Entrada (formato AFEX estándar)
{
agentFields: {
"receiverAccountType": "SAVINGS",
"receiverAccountType_label": "Cuenta de Ahorros"
}
}
// Transformación para Agente A
{
"bankType": "SAVINGS",
"bankType_label": "Cuenta de Ahorros"
}
// Transformación para Agente B
{
"institutionType": "CREDIT_UNION",
"institutionType_label": "Cuenta de Ahorros"
}
```
**Llamadas al servicio:**
```typescript
// Para Agente A
const transformedForAgentA = await agentFieldsService
.transformTransfersAgentFieldsValuesForAgent(
"AGENT_A",
[
{
id: "transfer123",
countryId: "CO",
methodPaymentId: 1,
agentFieldsValues: {
"receiverAccountType": "CHECKING"
}
}
]
);
// Para Agente B
const transformedForAgentB = await agentFieldsService
.transformTransfersAgentFieldsValuesForAgent(
"AGENT_B",
[
{
id: "transfer123",
countryId: "CO",
methodPaymentId: 1,
agentFieldsValues: {
"receiverAccountType": "CHECKING"
}
}
]
);
```
**Resultado: Una sola transferencia, múltiples formatos**
- El **usuario** ve una interfaz unificada con "Cuenta Corriente" y "Cuenta de Ahorros"
- **Agente A** recibe `bankType: "COMMERCIAL"` o `bankType: "SAVINGS"`
- **Agente B** recibe `institutionType: "BANK"` o `institutionType: "CREDIT_UNION"`
- **AFEX** mantiene consistencia interna con `receiverAccountType: "CHECKING"/"SAVINGS"`
### Caso 2: Transformación con Lógica Personalizada
**Entrada:**
```typescript
{
receiverBankCode: "001",
receiverBankSubCode: "123"
}
```
**Salida para Terrapay Canadá:**
```typescript
{
receiverBankCode: "001",
originalBankSubCode: "123",
receiverBankSubCode: "001123" // Código concatenado
}
```
## Flujos integrados
### Creación de giro (createTransfer)
```mermaid
sequenceDiagram
title Creación de giro (nuevo)
participant AFEX as AFEX+
participant TransferAPI as TransferAPI
participant AgentAPI as AgentAPI
participant Core as Core
AFEX ->> TransferAPI: createTransfer (request)
TransferAPI ->> TransferAPI: validateTransfer()
TransferAPI ->> TransferAPI: validateCompliance()
TransferAPI ->> AgentAPI: transformAgentFieldsForAgent (agentId, transfer)
AgentAPI ->> TransferAPI: transfer: Transfer
TransferAPI ->> TransferAPI: validateAgentFields()
TransferAPI ->> Core: createTransfer(transfer)
Core ->> TransferAPI:
TransferAPI ->> TransferAPI: tranferIntegrations()
TransferAPI ->> TransferAPI: saveTransfer
TransferAPI ->> AFEX: transfer: Transfer
```
### Obtención de giros pendiente de envío desde PulpoPlus (getPendingTransfersToSend)
Observación: En el siguiente diagrama se presentan 2 columnas dentro de **AgentAPI** para hacer la separación entre ejecuciones externas e internas de la propia *API*.
```mermaid
sequenceDiagram
title Obtener giros pendiente de envío (nuevo)
participant PulpoPlus as PulpoPlus
participant TransferAPI as TransferAPI
participant AgentAPI as AgentAPI
participant AgentAPI(internal) as AgentAPI(internal)
PulpoPlus ->> TransferAPI: getPendingTransfersToSend(agentId)
TransferAPI ->> AgentAPI: transformAgentFieldsForAgent (agentId, transfers)
AgentAPI ->> AgentAPI(internal): getAgentFields(agentId)
AgentAPI(internal) ->> AgentAPI: agentFields: AgentField[]
AgentAPI ->> AgentAPI(internal): [for each transfer] transformAgentFields(agentField, transfer)
AgentAPI(internal) ->> AgentAPI: transfers: Transfer[]
AgentAPI ->> TransferAPI: transfers: Transfer[]
TransferAPI ->> PulpoPlus: transfers: Transfer[]
```
### Lista de AfexFields
La implementación actual en la configuración de agentes disponibiliza los siguientes campos para ser utilizados mediante *afexFields*:
- **receiverProviderCode**: Código de banco para Depósito o *Wallet*
---
## Beneficios
- **Interoperabilidad**: Una transferencia funciona con múltiples agentes
- **Mantenimiento**: Lógica centralizada y configuración declarativa
- **Escalabilidad**: Agregar nuevos agentes sin cambiar código base
- **Consistencia**: Validación y transformación uniforme
- **Experiencia de usuario**: Formulario único independiente del agente
---
## Conclusión
El sistema de **afexFields** representa una solución robusta y escalable para la interoperabilidad multi-agente en el ecosistema de transferencias financieras. Su diseño modular y configuración declarativa permite adaptarse dinámicamente a los requisitos específicos de cada agente, mientras mantiene la consistencia y simplicidad para los usuarios finales.
Esta arquitectura facilita la expansión del negocio a nuevos mercados y agentes, reduciendo significativamente el tiempo de integración y los costos de mantenimiento, mientras garantiza la calidad y confiabilidad de las operaciones financieras.