Skip to content

Guía Completa de Integración de Perfiles

Esta guía cubre todos los flujos de trabajo relacionados con perfiles, formularios y validación de datos en Kuenta. Incluye ejemplos prácticos para:

  • Creación de clientes (debtors)
  • Actualización de perfiles
  • Manejo de formularios con missingFields
  • Solicitudes de crédito con validación dinámica

Conceptos Fundamentales

Arquitectura de Perfiles

Entidad (Entity)
├── Perfil Maestro (Master Profile)
│   ├── Natural (persona natural)
│   └── Legal (persona jurídica)
├── Perfiles Compartidos (Shared Profiles)
│   └── Por cada organización donde participa
└── Copias de Perfil (Profile Copies)
    └── Snapshots inmutables para créditos

IDs Importantes

IDDescripciónUso
entityIDID de la entidad (persona/empresa)Identificador principal
profileIDID del perfil específicoPara actualizaciones de perfil
configOrgIDID de la organización de marca blancaPara cargar formularios
debtorIDID del deudor (alias de entityID)En endpoints de deudores

1. Creación de Clientes (Debtors)

Flujo Completo

1. Crear deudor (POST /debtor)

2. Cargar formularios de la marca blanca

3. Actualizar perfil con datos requeridos

4. Perfil listo para solicitar créditos

Paso 1: Crear el Deudor

Endpoint: POST /debtor

bash
curl -X POST "https://api.kuenta.co/v1/debtor" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Config-Organization-ID: $CONFIG_ORG_ID" \
  -H "Organization-ID: $CREDITOR_ID" \
  -d '{
    "type": "natural",
    "idType": "CC",
    "idNumber": "1234567890"
  }'

Respuesta:

json
{
  "data": {
    "debtor": {
      "id": "770e8400-e29b-41d4-a716-446655440002",
      "type": "natural",
      "idType": "CC",
      "idNumber": "1234567890",
      "status": "pending",
      "parentID": "660e8400-e29b-41d4-a716-446655440001",
      "profile": {
        "id": "880e8400-e29b-41d4-a716-446655440003",
        "name": "",
        "email": "",
        "phone": ""
      }
    }
  },
  "success": true
}

Nota: El perfil se crea vacío. Debes completarlo con los campos requeridos por la marca blanca.

Paso 2: Cargar Formularios de la Marca Blanca

Endpoint: GET /entities/{configOrgID}/config/forms?type=natural

javascript
// Cargar formularios para persona natural
const response = await fetch(
  `${API_URL}/entities/${configOrgID}/config/forms?type=natural`,
  {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Config-Organization-ID': configOrgID,
      'Organization-ID': creditorID
    }
  }
);

const { data: { form } } = await response.json();
// form.fields contiene todos los campos configurados

Respuesta:

json
{
  "status": "success",
  "data": {
    "form": {
      "ID": "form-uuid",
      "name": "Formulario Combinado",
      "fields": [
        {
          "ID": "formfield-uuid-1",
          "fieldID": "field-uuid-1",
          "actived": true,
          "required": true,
          "field": {
            "name": "firstName",
            "label": "Primer Nombre",
            "type": "text"
          }
        },
        {
          "ID": "formfield-uuid-2",
          "fieldID": "field-uuid-2",
          "actived": true,
          "required": true,
          "field": {
            "name": "lastName",
            "label": "Apellido",
            "type": "text"
          }
        },
        {
          "ID": "formfield-uuid-3",
          "fieldID": "field-uuid-3",
          "actived": true,
          "required": false,
          "field": {
            "name": "incomeMonthly",
            "label": "Ingreso Mensual",
            "type": "currency"
          }
        }
      ]
    }
  }
}

Paso 3: Actualizar el Perfil

Endpoint: PUT /debtor/{debtorID}/profile

javascript
const updateProfile = async (debtorID, profileData, missingFields) => {
  const response = await fetch(
    `${API_URL}/debtor/${debtorID}/profile`,
    {
      method: 'PUT',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
        'Config-Organization-ID': configOrgID,
        'Organization-ID': creditorID
      },
      body: JSON.stringify({
        natural: profileData,
        missingFields: missingFields
      })
    }
  );
  
  return response.json();
};

// Ejemplo de uso
const profileData = {
  firstName: "Juan",
  lastName: "Pérez",
  email: "[email protected]",
  mobilePhone: "3001234567",
  incomeMonthly: 3500000  // Campo numérico
};

// missingFields: campos que fueron "preguntados" en el formulario
const missingFields = form.fields.map(f => ({
  fieldID: f.fieldID,
  name: f.field.name
}));

await updateProfile(debtorID, profileData, missingFields);

Payload completo:

json
{
  "natural": {
    "firstName": "Juan",
    "lastName": "Pérez",
    "email": "[email protected]",
    "mobilePhone": "3001234567",
    "incomeMonthly": 3500000,
    "idType": 1,
    "idNumber": "1234567890"
  },
  "missingFields": [
    { "fieldID": "field-uuid-1", "name": "firstName" },
    { "fieldID": "field-uuid-2", "name": "lastName" },
    { "fieldID": "field-uuid-3", "name": "incomeMonthly" }
  ]
}

2. Actualización de Perfiles

Tipos de Actualización

TipoEndpointCaso de Uso
Perfil propioPUT /profileUsuario actualiza su propio perfil
Perfil de deudorPUT /debtor/{id}/profileAcreedor actualiza perfil de deudor
Campo específicoPUT /debtor/{id}/profiles/{profileID}/fieldActualizar un solo campo
Perfil de organizaciónPUT /organization/{id}/profile/{profileID}Admin actualiza miembro

Actualización de Perfil Propio

Endpoint: PUT /profile

javascript
const updateOwnProfile = async (profileData) => {
  // 1. Cargar formularios disponibles
  const formsResponse = await fetch(
    `${API_URL}/entities/${configOrgID}/config/forms?type=natural`,
    { headers: authHeaders }
  );
  const { data: { form } } = await formsResponse.json();
  
  // 2. Construir missingFields de los campos que se actualizarán
  const missingFields = form.fields
    .filter(f => f.actived && f.field.name in profileData)
    .map(f => ({ fieldID: f.fieldID, name: f.field.name }));
  
  // 3. Enviar actualización
  const response = await fetch(`${API_URL}/profile`, {
    method: 'PUT',
    headers: {
      ...authHeaders,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      natural: profileData,
      missingFields: missingFields
    })
  });
  
  return response.json();
};

Actualización de Campo Específico

Cuando solo necesitas actualizar un campo:

Endpoint: PUT /debtor/{debtorID}/profiles/{profileID}/field

javascript
const updateSingleField = async (debtorID, profileID, fieldName, fieldValue) => {
  const response = await fetch(
    `${API_URL}/debtor/${debtorID}/profiles/${profileID}/field`,
    {
      method: 'PUT',
      headers: {
        ...authHeaders,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        field: fieldName,
        value: fieldValue
      })
    }
  );
  
  return response.json();
};

// Ejemplo: Actualizar solo el email
await updateSingleField(debtorID, profileID, 'email', '[email protected]');

3. Solicitudes de Crédito con MissingFields

El flujo de solicitud de crédito puede requerir información adicional del perfil. El sistema utiliza missingFields para indicar qué datos faltan.

Flujo de Solicitud con Validación Dinámica

1. Crear solicitud de crédito (POST /receivables o POST /payables)

2. Si faltan datos → Status: AwaitingForm (13)

3. Recibir missingFields en la respuesta

4. Mostrar formulario con SOLO los campos faltantes

5. Enviar datos y reintentar

6. Repetir hasta que no haya missingFields

Crear Solicitud de Crédito

Endpoint: POST /receivables (perspectiva del acreedor)

javascript
const createCredit = async (creditData) => {
  const response = await fetch(`${API_URL}/receivables`, {
    method: 'POST',
    headers: {
      ...authHeaders,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(creditData)
  });
  
  const result = await response.json();
  
  // Verificar si hay campos faltantes
  if (result.data.credit.status === 13) { // AwaitingForm
    return {
      status: 'awaiting_form',
      credit: result.data.credit,
      missingFields: result.data.missingFields,
      profile: result.data.profile
    };
  }
  
  return {
    status: 'success',
    credit: result.data.credit
  };
};

// Ejemplo de uso
const creditResult = await createCredit({
  debtorID: "debtor-uuid",
  creditLineID: "credit-line-uuid",
  principal: 5000000,
  time: 90,
  rate: 0.025
});

if (creditResult.status === 'awaiting_form') {
  // Mostrar formulario con missingFields
  showMissingFieldsForm(creditResult.missingFields, creditResult.profile);
}

Respuesta con MissingFields

Cuando el perfil está incompleto:

json
{
  "data": {
    "credit": {
      "ID": "credit-uuid",
      "status": 13,
      "statusDescription": "Esperando información del formulario"
    },
    "missingFields": [
      {
        "ID": "formfield-uuid-1",
        "fieldID": "field-uuid-1",
        "formID": "form-uuid",
        "actived": true,
        "required": true,
        "field": {
          "name": "occupation",
          "label": "Ocupación",
          "type": "text"
        }
      },
      {
        "ID": "formfield-uuid-2",
        "fieldID": "field-uuid-2",
        "formID": "form-uuid",
        "actived": true,
        "required": true,
        "field": {
          "name": "incomeMonthly",
          "label": "Ingreso Mensual",
          "type": "currency"
        }
      }
    ],
    "profile": {
      "ID": "profile-uuid",
      "firstName": "Juan",
      "lastName": "Pérez"
    }
  }
}

Completar Formulario con MissingFields

Importante: Solo mostrar y enviar los campos que están en missingFields.

javascript
const completeMissingFields = async (creditID, missingFields, formValues) => {
  // 1. Filtrar solo los campos que fueron "preguntados"
  const profileData = {};
  const fieldsToSend = [];
  
  for (const field of missingFields) {
    const fieldName = field.field.name;
    if (fieldName in formValues) {
      profileData[fieldName] = formValues[fieldName];
      fieldsToSend.push({
        fieldID: field.fieldID,
        name: fieldName
      });
    }
  }
  
  // 2. Actualizar perfil del deudor
  const updateResponse = await fetch(
    `${API_URL}/payables/${creditID}/updates/profile`,
    {
      method: 'PUT',
      headers: {
        ...authHeaders,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        natural: profileData,
        missingFields: fieldsToSend
      })
    }
  );
  
  return updateResponse.json();
};

// Ejemplo: Usuario completa los campos faltantes
const formValues = {
  occupation: "Ingeniero de Software",
  incomeMonthly: 8500000
};

const result = await completeMissingFields(creditID, missingFields, formValues);

// Verificar si hay más campos faltantes
if (result.data.credit.status === 13) {
  // Aún faltan campos
  showMissingFieldsForm(result.data.missingFields, result.data.profile);
} else {
  // Proceso continúa
  console.log('Perfil completo, crédito en análisis');
}

Endpoint para Actualizar durante Solicitud

Perspectiva del deudor: PUT /payables/{creditID}/updates/{ask}

bash
curl -X PUT "https://api.kuenta.co/v1/payables/{creditID}/updates/profile" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "natural": {
      "occupation": "Ingeniero de Software",
      "incomeMonthly": 8500000,
      "company": "Tech Corp"
    },
    "missingFields": [
      { "fieldID": "field-uuid-1", "name": "occupation" },
      { "fieldID": "field-uuid-2", "name": "incomeMonthly" },
      { "fieldID": "field-uuid-3", "name": "company" }
    ]
  }'

4. Manejo de Campos Numéricos

Los campos numéricos requieren manejo especial:

Frontend: Conversión antes de enviar

javascript
// Campos numéricos conocidos
const numericFields = [
  'incomeMonthly', 'variableIncome', 'otherIncome', 'spouseIncome',
  'expenses', 'otherExpenses', 'assets', 'liabilities', 'worth'
];

const prepareProfileData = (formValues, fields) => {
  const result = { ...formValues };
  
  // Convertir campos vacíos a 0 para campos numéricos
  for (const fieldName of numericFields) {
    if (fieldName in result) {
      if (result[fieldName] === '' || result[fieldName] === null || result[fieldName] === undefined) {
        result[fieldName] = 0;
      }
    }
  }
  
  // Eliminar campos vacíos no numéricos
  for (const [key, value] of Object.entries(result)) {
    if (value === '' || value === null || value === undefined) {
      if (!numericFields.includes(key)) {
        delete result[key];
      }
    }
  }
  
  return result;
};

Backend: Limpieza automática

El backend convierte automáticamente strings vacíos a null:

json
// Enviado por frontend
{ "incomeMonthly": "", "spouseIncome": "" }

// Procesado por backend
{ "incomeMonthly": null, "spouseIncome": null }

5. Flujos Completos por Caso de Uso

Caso A: Registro de nuevo cliente y solicitud de crédito

javascript
async function registerClientAndApplyCredit(clientData, creditData) {
  // 1. Crear el deudor
  const debtor = await createDebtor({
    type: 'natural',
    idType: clientData.idType,
    idNumber: clientData.idNumber
  });
  
  // 2. Cargar formularios de la marca blanca
  const forms = await loadForms(configOrgID, 'natural');
  
  // 3. Actualizar perfil con datos iniciales
  await updateDebtorProfile(debtor.id, clientData.profile, forms.fields);
  
  // 4. Crear solicitud de crédito
  let creditResult = await createCredit({
    debtorID: debtor.id,
    ...creditData
  });
  
  // 5. Manejar campos faltantes si es necesario
  while (creditResult.status === 'awaiting_form') {
    // Mostrar formulario al usuario
    const additionalData = await promptUserForFields(creditResult.missingFields);
    
    // Enviar datos adicionales
    creditResult = await completeMissingFields(
      creditResult.credit.ID,
      creditResult.missingFields,
      additionalData
    );
  }
  
  return creditResult;
}

Caso B: Usuario edita su perfil desde cuenta

javascript
async function editOwnProfile(newData) {
  // 1. Cargar formularios según profileEditScope de la organización
  const forms = await loadForms(configOrgID, 'natural');
  // El backend ya filtró según profileEditScope (all o base)
  
  // 2. Mostrar formulario con campos disponibles
  const formUI = buildForm(forms.fields);
  
  // 3. Usuario modifica datos
  const updatedValues = await getUserInput(formUI);
  
  // 4. Preparar datos (convertir numéricos, limpiar vacíos)
  const preparedData = prepareProfileData(updatedValues, forms.fields);
  
  // 5. Construir missingFields solo con campos modificados
  const modifiedFields = forms.fields.filter(f => 
    f.field.name in preparedData
  ).map(f => ({
    fieldID: f.fieldID,
    name: f.field.name
  }));
  
  // 6. Enviar actualización
  return await updateOwnProfile({
    natural: preparedData,
    missingFields: modifiedFields
  });
}

Caso C: Acreedor actualiza perfil de deudor

javascript
async function updateDebtorByCreditor(debtorID, updates) {
  // 1. Obtener perfil actual del deudor
  const currentProfile = await getDebtorProfile(debtorID);
  
  // 2. Cargar formularios de la marca blanca
  const forms = await loadForms(configOrgID, currentProfile.type);
  
  // 3. Validar que los campos existen en el formulario
  const validFields = forms.fields.filter(f => 
    f.actived && f.field.name in updates
  );
  
  if (validFields.length === 0) {
    throw new Error('No hay campos válidos para actualizar');
  }
  
  // 4. Preparar payload
  const profileData = {};
  const missingFields = [];
  
  for (const field of validFields) {
    const fieldName = field.field.name;
    profileData[fieldName] = updates[fieldName];
    missingFields.push({
      fieldID: field.fieldID,
      name: fieldName
    });
  }
  
  // 5. Enviar actualización
  return await fetch(`${API_URL}/debtor/${debtorID}/profile`, {
    method: 'PUT',
    headers: {
      ...authHeaders,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      natural: profileData,
      missingFields: missingFields
    })
  });
}

6. Control de ProfileEditScope

Configuración por Organización

El campo profileEditScope controla qué campos pueden editar los usuarios:

ValorComportamiento
all (defecto)Todos los formularios combinados
baseSolo el formulario básico de la marca blanca

Verificar Configuración

javascript
const getProfileEditScope = async (configOrgID) => {
  const response = await fetch(
    `${API_URL}/organization/${configOrgID}/config`,
    { headers: authHeaders }
  );
  
  const { data } = await response.json();
  return data.configuration.profileEditScope || 'all';
};

Cargar Formularios según Scope

El endpoint /entities/{id}/config/forms respeta automáticamente profileEditScope:

javascript
// Si profileEditScope = 'all': retorna todos los formularios mergeados
// Si profileEditScope = 'base': retorna solo el formulario básico

const forms = await fetch(
  `${API_URL}/entities/${configOrgID}/config/forms?type=natural`,
  { headers: authHeaders }
);

Forzar Formulario Básico

Para ignorar profileEditScope y obtener solo el básico:

javascript
const basicFormOnly = await fetch(
  `${API_URL}/entities/${configOrgID}/config/forms?type=natural&name=base`,
  { headers: authHeaders }
);

7. Validación de Formularios

Validación en Frontend

javascript
const validateField = (field, value) => {
  const errors = [];
  
  // Requerido
  if (field.required && (value === '' || value === null || value === undefined)) {
    errors.push(`${field.field.label} es requerido`);
  }
  
  // Validación por tipo
  switch (field.field.type) {
    case 'email':
      if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
        errors.push('Email inválido');
      }
      break;
      
    case 'phone':
      if (value && !/^\+?[\d\s-]{7,15}$/.test(value)) {
        errors.push('Teléfono inválido');
      }
      break;
      
    case 'number':
    case 'currency':
      if (value !== '' && isNaN(Number(value))) {
        errors.push('Debe ser un número válido');
      }
      break;
  }
  
  // Validación personalizada
  if (field.field.validation) {
    const { pattern, min, max, minLength, maxLength } = field.field.validation;
    
    if (pattern && value && !new RegExp(pattern).test(value)) {
      errors.push('Formato inválido');
    }
    
    if (min !== undefined && Number(value) < min) {
      errors.push(`Valor mínimo: ${min}`);
    }
    
    if (max !== undefined && Number(value) > max) {
      errors.push(`Valor máximo: ${max}`);
    }
    
    if (minLength && value && value.length < minLength) {
      errors.push(`Mínimo ${minLength} caracteres`);
    }
    
    if (maxLength && value && value.length > maxLength) {
      errors.push(`Máximo ${maxLength} caracteres`);
    }
  }
  
  return errors;
};

const validateForm = (fields, values) => {
  const allErrors = {};
  let hasErrors = false;
  
  for (const field of fields) {
    if (!field.actived) continue;
    
    const fieldName = field.field.name;
    const value = values[fieldName];
    const errors = validateField(field, value);
    
    if (errors.length > 0) {
      allErrors[fieldName] = errors;
      hasErrors = true;
    }
  }
  
  return { valid: !hasErrors, errors: allErrors };
};

Códigos de Error Comunes

CódigoDescripciónSolución
400 BindErrorJSON malformado o tipos incorrectosVerificar estructura del payload
400 ValidationErrorValores no cumplen validaciónVerificar reglas del campo
409 ConflictEmail o ID duplicadoEl valor ya existe
422 BusinessRuleViolationRegla de negocio violadaRevisar políticas

8. Ejemplos Completos

Python: Cliente completo para gestión de perfiles

python
import requests
from typing import Dict, List, Optional
from dataclasses import dataclass

@dataclass
class KuentaClient:
    base_url: str
    access_token: str
    config_org_id: str
    organization_id: str
    
    @property
    def headers(self) -> Dict:
        return {
            'Authorization': f'Bearer {self.access_token}',
            'Content-Type': 'application/json',
            'Config-Organization-ID': self.config_org_id,
            'Organization-ID': self.organization_id
        }
    
    def create_debtor(self, entity_type: str, id_type: str, id_number: str) -> Dict:
        """Crear nuevo deudor"""
        response = requests.post(
            f'{self.base_url}/debtor',
            headers=self.headers,
            json={
                'type': entity_type,
                'idType': id_type,
                'idNumber': id_number
            }
        )
        response.raise_for_status()
        return response.json()['data']['debtor']
    
    def get_forms(self, entity_type: str = 'natural') -> Dict:
        """Obtener formularios de la marca blanca"""
        response = requests.get(
            f'{self.base_url}/entities/{self.config_org_id}/config/forms',
            headers=self.headers,
            params={'type': entity_type}
        )
        response.raise_for_status()
        return response.json()['data']['form']
    
    def update_debtor_profile(
        self, 
        debtor_id: str, 
        profile_data: Dict, 
        fields: List[Dict]
    ) -> Dict:
        """Actualizar perfil de deudor"""
        # Construir missingFields
        missing_fields = [
            {'fieldID': f['fieldID'], 'name': f['field']['name']}
            for f in fields
            if f['actived'] and f['field']['name'] in profile_data
        ]
        
        response = requests.put(
            f'{self.base_url}/debtor/{debtor_id}/profile',
            headers=self.headers,
            json={
                'natural': profile_data,
                'missingFields': missing_fields
            }
        )
        response.raise_for_status()
        return response.json()
    
    def create_credit(self, credit_data: Dict) -> Dict:
        """Crear solicitud de crédito"""
        response = requests.post(
            f'{self.base_url}/receivables',
            headers=self.headers,
            json=credit_data
        )
        response.raise_for_status()
        return response.json()['data']
    
    def complete_missing_fields(
        self, 
        credit_id: str, 
        profile_data: Dict,
        missing_fields: List[Dict]
    ) -> Dict:
        """Completar campos faltantes de una solicitud"""
        fields_to_send = [
            {'fieldID': f['fieldID'], 'name': f['field']['name']}
            for f in missing_fields
            if f['field']['name'] in profile_data
        ]
        
        response = requests.put(
            f'{self.base_url}/payables/{credit_id}/updates/profile',
            headers=self.headers,
            json={
                'natural': profile_data,
                'missingFields': fields_to_send
            }
        )
        response.raise_for_status()
        return response.json()['data']


# Ejemplo de uso
if __name__ == '__main__':
    client = KuentaClient(
        base_url='https://api.kuenta.co/v1',
        access_token='your_token',
        config_org_id='config-org-uuid',
        organization_id='org-uuid'
    )
    
    # 1. Crear deudor
    debtor = client.create_debtor('natural', 'CC', '1234567890')
    print(f"Deudor creado: {debtor['id']}")
    
    # 2. Cargar formularios
    form = client.get_forms('natural')
    print(f"Formulario cargado con {len(form['fields'])} campos")
    
    # 3. Actualizar perfil
    profile_data = {
        'firstName': 'Juan',
        'lastName': 'Pérez',
        'email': '[email protected]',
        'mobilePhone': '3001234567',
        'incomeMonthly': 5000000
    }
    
    result = client.update_debtor_profile(debtor['id'], profile_data, form['fields'])
    print("Perfil actualizado exitosamente")
    
    # 4. Crear crédito
    credit_result = client.create_credit({
        'debtorID': debtor['id'],
        'creditLineID': 'credit-line-uuid',
        'principal': 10000000,
        'time': 180,
        'rate': 0.025
    })
    
    # 5. Manejar campos faltantes si es necesario
    if credit_result['credit']['status'] == 13:  # AwaitingForm
        print("Faltan campos, completando...")
        additional_data = {
            'occupation': 'Ingeniero',
            'company': 'Tech Corp'
        }
        client.complete_missing_fields(
            credit_result['credit']['ID'],
            additional_data,
            credit_result['missingFields']
        )

JavaScript/TypeScript: Módulo de gestión de perfiles

typescript
interface FormField {
  ID: string;
  fieldID: string;
  actived: boolean;
  required: boolean;
  field: {
    name: string;
    label: string;
    type: string;
    validation?: {
      pattern?: string;
      min?: number;
      max?: number;
      minLength?: number;
      maxLength?: number;
    };
  };
}

interface ProfileData {
  [key: string]: any;
}

class KuentaProfileManager {
  constructor(
    private baseUrl: string,
    private accessToken: string,
    private configOrgId: string,
    private organizationId: string
  ) {}

  private get headers(): HeadersInit {
    return {
      'Authorization': `Bearer ${this.accessToken}`,
      'Content-Type': 'application/json',
      'Config-Organization-ID': this.configOrgId,
      'Organization-ID': this.organizationId
    };
  }

  // Campos numéricos que deben convertirse de "" a 0
  private numericFields = new Set([
    'incomeMonthly', 'variableIncome', 'otherIncome', 'spouseIncome',
    'expenses', 'otherExpenses', 'assets', 'liabilities', 'worth',
    'mortgageCommercialValue', 'pledgeCommercialValue'
  ]);

  /**
   * Prepara datos del perfil para envío
   * - Convierte campos numéricos vacíos a 0
   * - Elimina campos de texto vacíos
   */
  prepareProfileData(data: ProfileData): ProfileData {
    const result: ProfileData = {};
    
    for (const [key, value] of Object.entries(data)) {
      if (this.numericFields.has(key)) {
        // Campos numéricos: vacío → 0
        result[key] = (value === '' || value === null || value === undefined) 
          ? 0 
          : Number(value);
      } else if (value !== '' && value !== null && value !== undefined) {
        // Otros campos: solo incluir si tienen valor
        result[key] = value;
      }
    }
    
    return result;
  }

  /**
   * Construye el array de missingFields para el payload
   */
  buildMissingFields(fields: FormField[], data: ProfileData): Array<{fieldID: string, name: string}> {
    return fields
      .filter(f => f.actived && f.field.name in data)
      .map(f => ({
        fieldID: f.fieldID,
        name: f.field.name
      }));
  }

  /**
   * Carga formularios de la marca blanca
   */
  async loadForms(type: 'natural' | 'legal'): Promise<{fields: FormField[]}> {
    const response = await fetch(
      `${this.baseUrl}/entities/${this.configOrgId}/config/forms?type=${type}`,
      { headers: this.headers }
    );
    
    if (!response.ok) {
      throw new Error(`Error loading forms: ${response.status}`);
    }
    
    const { data } = await response.json();
    return data.form;
  }

  /**
   * Crea un nuevo deudor
   */
  async createDebtor(type: 'natural' | 'legal', idType: string, idNumber: string) {
    const response = await fetch(`${this.baseUrl}/debtor`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify({ type, idType, idNumber })
    });
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Error creating debtor');
    }
    
    const { data } = await response.json();
    return data.debtor;
  }

  /**
   * Actualiza el perfil de un deudor
   */
  async updateDebtorProfile(
    debtorId: string, 
    profileData: ProfileData, 
    fields: FormField[]
  ) {
    const preparedData = this.prepareProfileData(profileData);
    const missingFields = this.buildMissingFields(fields, preparedData);
    
    const response = await fetch(`${this.baseUrl}/debtor/${debtorId}/profile`, {
      method: 'PUT',
      headers: this.headers,
      body: JSON.stringify({
        natural: preparedData,
        missingFields
      })
    });
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Error updating profile');
    }
    
    return response.json();
  }

  /**
   * Flujo completo: crear deudor y completar perfil
   */
  async registerClient(
    idType: string,
    idNumber: string,
    profileData: ProfileData,
    type: 'natural' | 'legal' = 'natural'
  ) {
    // 1. Crear deudor
    const debtor = await this.createDebtor(type, idType, idNumber);
    
    // 2. Cargar formularios
    const form = await this.loadForms(type);
    
    // 3. Actualizar perfil
    await this.updateDebtorProfile(debtor.id, profileData, form.fields);
    
    return debtor;
  }
}

// Uso
const manager = new KuentaProfileManager(
  'https://api.kuenta.co/v1',
  'access_token',
  'config-org-id',
  'organization-id'
);

// Registrar cliente completo
const debtor = await manager.registerClient('CC', '1234567890', {
  firstName: 'María',
  lastName: 'González',
  email: '[email protected]',
  mobilePhone: '3109876543',
  incomeMonthly: 4500000,
  occupation: 'Contadora'
});

console.log('Cliente registrado:', debtor.id);

9. Solución de Problemas

Error: "failed on 'numeric' tag"

Causa: Campo numérico recibió string vacío.

Solución:

javascript
// Antes de enviar, convertir vacíos a 0 o null
if (field.type === 'currency' || field.type === 'number') {
  value = value === '' ? 0 : Number(value);
}

Error: "Campo no encontrado"

Causa: El campo existe en el perfil pero no en el formulario de la marca blanca.

Solución: Verificar que el campo esté activado en los formularios:

javascript
const fieldExists = form.fields.some(f => 
  f.actived && f.field.name === fieldName
);

Error: "Perfil no sincronizado"

Causa: La sincronización entre perfiles maestro y compartido no ha completado.

Solución: Esperar unos segundos y reintentar, o usar el perfil maestro directamente.

MissingFields vacío pero crédito rechazado

Causa: Reglas de negocio (no de formulario) fallaron.

Verificar:

  • Límites de la línea de crédito
  • Scoring del cliente
  • Políticas de la organización