Bentian Bentian | Documentación
Descargar v0.3.8
Especificación Enterprise · 9 min de lectura · Actualizado v0.3.8

Endpoint Universal: Especificación Técnica y Protocolo Bidireccional

Norma de integración Enterprise para tiendas a medida (PHP, Laravel, Node.js, Plesk): ciclo ACK, idempotencia SQLite, diccionario de datos, HMAC-SHA256 y resiliencia.

No todos los comercios electrónicos operan sobre plataformas cerradas o plugins prefabricados. Miles de empresas gestionan sus ventas mediante plataformas B2B y B2C a medida desarrolladas en PHP nativo, Laravel, Symfony, Node.js, Python o arquitecturas desacopladas (Angular, React, Vue, Next.js) alojadas en servidores Plesk, cPanel o VPS dedicados. El Endpoint Universal de Bentian ERP Bridge define el estándar formal de comunicación HTTP/JSON para integrar cualquier aplicación web personalizada con Factusol con garantías transaccionales corporativas.

1. Propósito y Arquitectura de Integración Enterprise

A diferencia de las integraciones basadas en webhooks entrantes que exigen abrir puertos en el router del almacén o contratar costosas IPs fijas con configuraciones de DMZ peligrosas, la arquitectura de Bentian opera exclusivamente mediante tráfico HTTPS saliente desde el puesto de trabajo hacia la tienda online:

[ Servidor Web (Plesk / VPS) ] ◀──(HTTPS Saliente / TLS 1.3)── [ Agente Bentian (Almacén) ] ──(OLEDB)──▶ [ Factusol (.accdb) ]
✓ Cero puertos abiertos en el router local · Cero exposición de Windows a Internet · Comunicación cifrada extremo a extremo

El servidor web de la tienda solo necesita exponer un script HTTPS (por ejemplo, erp-bridge-endpoint.php o una ruta de API en su framework) capaz de responder a consultas de lectura de pedidos y recibir actualizaciones de inventario y confirmaciones de estado.

2. Protocolo Bidireccional de Comunicación (Polling & Flujo ACK)

La sincronización de pedidos entre la tienda web y Factusol se rige por un protocolo bidireccional en dos fases con cierre explícito (Two-Phase Commit / ACK):

  1. Fase 1 (Sondeo / Pull): El agente solicita pedidos en espera enviando una petición GET /api/bentian-orders.php?limit=50&status=PENDING (o ?action=get_orders).
  2. Fase 2 (Custodia Local): El agente almacena el bloque de pedidos en su base de datos local SQLite (queue.db) en modo WAL.
  3. Fase 3 (Inyección Atómica): El conector Factusol abre una transacción OLEDB (BeginTrans), busca o crea el cliente en F_CLI, inserta la cabecera en F_PCL y las líneas en F_LPC, y consolida la operación con CommitTrans.
  4. Fase 4 (Confirmación / ACK): El agente emite una petición POST /api/bentian-ack.php (o ?action=ack_orders) notificando a la tienda online que el pedido ha sido registrado con éxito en Factusol y facilitando el número de pedido oficial del ERP.
  5. Fase 5 (Transición en Web): La tienda web cambia el estado del pedido de PENDING a SYNCED, almacenando la referencia y fecha para auditoría.

3. Confirmación de Recepción (Flujo ACK) y Cierre de Transacción

Para cerrar formalmente el ciclo de vida del pedido, el agente de Bentian invoca el endpoint de confirmación enviando el siguiente contrato JSON:

Llamada Individual de Confirmación:
POST /api/bentian-ack.php HTTP/1.1
Host: www.tutienda.com
Content-Type: application/json; charset=utf-8
X-Bentian-Token: <TOKEN_SECRETO>
X-Bentian-Timestamp: 1791189000

{
  "orderId": "1042",
  "status": "synced",
  "factusolOrderNumber": "2026/0014",
  "factusolSeries": "1",
  "syncedAt": "2026-10-05T08:30:00Z"
}
Llamada en Bloque (Batch Confirmation para múltiples pedidos):
POST /api/erp-bridge-endpoint.php?action=ack_orders HTTP/1.1
Host: www.tutienda.com
Content-Type: application/json; charset=utf-8
X-Bentian-Token: <TOKEN_SECRETO>
X-Bentian-Timestamp: 1791189000

{
  "confirmations": [
    {
      "orderId": "1042",
      "status": "synced",
      "factusolOrderNumber": "2026/0014",
      "factusolSeries": "1",
      "syncedAt": "2026-10-05T08:30:00Z"
    },
    {
      "orderId": "1043",
      "status": "synced",
      "factusolOrderNumber": "2026/0015",
      "factusolSeries": "1",
      "syncedAt": "2026-10-05T08:30:02Z"
    }
  ]
}

Respuesta esperada por el agente (HTTP 200 OK):

{
  "success": true,
  "count": 2,
  "message": "Pedidos marcados como sincronizados correctamente"
}

4. Resiliencia ante Fallos de Red e Idempotencia en SQLite

En cualquier sistema distribuido sobre Internet, las confirmaciones de red pueden fallar: un corte de fibra de 2 segundos, una saturación temporal en el hosting o un error 502 Bad Gateway de un proxy inverso pueden impedir que el agente entregue el ACK a la tienda online tras haber insertado el pedido en Factusol.

El Dilema del ACK Fallido: Si la llamada ACK falla por un corte de red, la tienda web no se entera y mantendrá el pedido en estado PENDING. En el siguiente ciclo de sondeo, la tienda volverá a ofrecer el pedido al agente. ¿Cómo se evita que Factusol registre el pedido por segunda vez?

Bentian resuelve este dilema mediante idempotencia estricta en su cola local SQLite (queue.db):

  • Clave Primaria de Idempotencia: La tabla local de SQLite indexa unívocamente el identificador del pedido web (web_order_id).
  • Detección de Pedido ya Inyectado: Cuando el agente descarga el lote de pedidos y recibe de nuevo el pedido 1042, consulta SQLite antes de tocar Factusol. Al detectar que el pedido ya figura con estado INJECTED y cuenta con el número correlativo 2026/0014 asignado, el agente aborta inmediatamente cualquier inserción en F_PCL y F_LPC.
  • Reintento Exclusivo del ACK: El agente detecta que la única tarea pendiente es la confirmación remota. Por tanto, emite de nuevo la llamada POST /api/bentian-ack.php hacia la tienda web. En cuanto el servidor responde con HTTP 200, el agente marca el pedido como ACK_CONFIRMED.
  • Resultado Garantizado: Cero duplicaciones en Factusol, cero descuadres de existencias y entrega garantizada del estado final en la tienda web (*At-Least-Once Delivery con Efecto Exactly-Once en Factusol*).

5. Diccionario de Datos: Especificación Rigurosa de Campos JSON

A continuación se detalla la especificación de todos los campos del payload devuelto en la descarga de pedidos:

5.1 Cabecera del Pedido

Campo JSON Tipo Obligatorio Destino Factusol Descripción & Ejemplo
id String | Int OBLIGATORIO queue.db / SUFPCL Identificador unívoco del pedido en la tienda web. Clave de idempotencia (ej: "1042").
orderNumber String Opcional F_PCL.SUFPCL Código visual del pedido presentado al cliente (ej: "PED-2026-0891"). Si se omite, se usa id.
status String OBLIGATORIO Filtro de descarga Estado del pedido en la web. Debe ser "PENDING" o "processing" para su importación.
total Float OBLIGATORIO F_PCL.TOTPCL Importe total del pedido con IVA, recargos y portes incluidos (ej: 215.38).
subtotal Float Opcional F_PCL.NETPCL Base imponible neta total sin impuestos (ej: 178.00).
taxTotal Float Opcional F_PCL.IVAPCL Total acumulado de cuota de IVA (ej: 37.38).
shippingCost Float Opcional F_PCL.PORPCL / Línea Gastos de envío cobrados al cliente (ej: 6.50).
paymentMethod String Opcional F_PCL.FPOPCL Forma de pago empleada (ej: "redsys", "stripe", "bizum", "transfer").
paymentStatus String Opcional F_PCL.ESTPCL Estado del pago (ej: "COMPLETED", "PAID", "PENDING").
paymentReference String Opcional F_PCL.OBSPCL Identificador de transacción o autorización bancaria (ej: "ch_3NpA4...").
notes String Opcional F_PCL.OBSPCL Instrucciones de entrega u observaciones del comprador (ej: "Entregar por la mañana").
createdAt String ISO-8601 Opcional F_PCL.FECPCL Fecha y hora UTC de la orden (ej: "2026-10-05T08:14:22Z"). Si falta, se usa la fecha actual.

5.2 Objeto del Cliente (customer)

Campo JSON Tipo Obligatorio Destino Factusol Descripción & Mapeo
name / fullName String OBLIGATORIO F_CLI.NOFCLI / CNOFCL Nombre fiscal o comercial del cliente (ej: "Ferretería Industrial del Norte S.L.").
taxId / cif / nif String CRÍTICO F_CLI.NIFCLI / CNIPCL NIF, CIF o NIE español/europeo. Clave unívoca de búsqueda en F_CLI para no duplicar fichas.
email String Recomendado F_CLI.EMACLI / CEMPCL Email de facturación y seguimiento (ej: "[email protected]").
phone String Opcional F_CLI.TELCLI / TELPCL Teléfono móvil o fijo para el albarán de transporte (ej: "612345678").
address / street String Opcional F_CLI.DOMCLI / CDOPCL Dirección física de entrega y facturación (ej: "Polígono Industrial Nave 12").
postalCode / cp String Opcional F_CLI.CPOCLI / CCPPCL Código postal de 5 dígitos (ej: "28001").
city String Opcional F_CLI.POBCLI / CPOPCL Municipio o localidad (ej: "Madrid").
province / state String Opcional F_CLI.PROCLI / CPRPCL Provincia (ej: "Madrid").
country String (ISO 3166-1) Opcional F_CLI.PAICLI / CPAPCL Código de país de 2 letras en mayúsculas (por defecto "ES").

5.3 Array de Líneas de Pedido (lines)

Campo JSON Tipo Obligatorio Destino Factusol Regla de Negocio
sku / artlpc String OBLIGATORIO F_LPC.ARTLPC Código del artículo en el catálogo de Factusol (F_ART.CODART). Clave de cruce.
name / deslpc String Recomendado F_LPC.DESLPC Descripción textual de la línea (máx. 50 caracteres para caber en campos legacy de Access).
quantity / canlpc Float | Int OBLIGATORIO F_LPC.CANLPC Número de unidades solicitadas (debe ser mayor estricto que 0).
priceWithoutVat / prelpc Float OBLIGATORIO F_LPC.PRELPC Precio neto unitario antes de impuestos (ej: 89.00). Evita la trampa del doble IVA.
vatRate / ivalpc Float OBLIGATORIO F_LPC.IVALPC Porcentaje de IVA aplicable a la línea (valores estándar en España: 21.0, 10.0, 4.0, 0.0).
equivalenceSurcharge / reclpc Float Opcional F_LPC.RECLPC Porcentaje de Recargo de Equivalencia si el cliente está acogido (ej: 5.2, 1.4, 0.5).
discountPercent / dtolpc Float Opcional F_LPC.DTOLPC Porcentaje de descuento comercial aplicado a la línea (ej: 10.0 para un 10%).

6. Paginación de Alto Rendimiento, Límites y Versionado

Para garantizar que el conector pueda operar en tiendas con cientos de pedidos diarios sin provocar caídas de memoria (memory_limit) en servidores compartidos, el Endpoint Universal incorpora paginación basada en cursor y límites estrictos:

  • Límite por Defecto: Si no se especifica, el servidor debe retornar hasta 50 pedidos por bloque.
  • Límite Máximo Estricto: El parámetro limit queda acotado a un máximo de 200 pedidos (max(1, min(200, intval($_GET['limit'])))). Cualquier solicitud superior es truncada a 200.
  • Paginación por Cursor: El parámetro cursor o since_id permite solicitar los pedidos posteriores al último ID procesado (ej: GET ?action=get_orders&limit=50&cursor=1042).
  • Versionado Semántico del Esquema: Cada respuesta incluye el campo "version": "1.1.0". El agente utiliza una política de compatibilidad hacia adelante (*forward-compatibility*): los campos JSON nuevos o no reconocidos son ignorados silenciosamente sin romper el deserializador.

7. Seguridad: Headers, Criptografía HMAC-SHA256 y Anti-Replay

Todas las peticiones HTTP emitidas por el agente hacia el endpoint incluyen un conjunto estándar de cabeceras de seguridad criptográfica:

Cabecera HTTP Formato Obligatoriedad Propósito de Seguridad
X-Bentian-Token String alfanumérico OBLIGATORIO Token secreto pre-compartido entre el agente local y el servidor web.
X-Bentian-Timestamp Época UNIX en segundos OBLIGATORIO Protección Anti-Replay: el servidor rechaza peticiones con más de 300s de desfase.
X-Bentian-Signature Hexadecimal 64 caracteres Recomendado (HMAC) Firma criptográfica HMAC-SHA256 calculada sobre timestamp + '.' + body.
X-Bentian-Agent-Version String semver (ej: v0.3.8) Informativo Permite al servidor web auditar qué versión del agente está conectándose.
Protección Anti-Replay de 300 Segundos: El servidor web calcula abs(time() - $requestTimestamp). Si la diferencia es superior a 300 segundos (5 minutos), la petición se descarta inmediatamente con código HTTP 403 Forbidden. Esto impide que un atacante que intercepte un paquete previo pueda replicarlo para descargar pedidos o alterar stock.

8. Rotación de Secretos y Variables de Entorno

Para cumplir con las normas de seguridad ISO 27001 y RGPD:

  • Prohibición de Secretos en el Código: El token de Bentian NUNCA debe quedar grabado en texto plano dentro de scripts PHP accesibles públicamente o subidos a GitHub/GitLab.
  • Almacenamiento Canónico en Variables de Entorno: Debe configurarse en el entorno del sistema operativo o en el panel de hosting (Plesk → Configuración de PHP → Variables de entorno, o fichero .env ubicado por encima de la carpeta pública httpdocs):
    BENTIAN_ENDPOINT_TOKEN="eb_sec_9f83b2a1c4e7d6f5..."
  • Rotación sin Caída de Servicio (Zero-Downtime Rotation): Cuando se desee rotar un secreto empresarial, el servidor web puede aceptar temporalmente dos claves durante un período de solapamiento de 24 horas:
    $validTokens = [
        getenv('BENTIAN_ENDPOINT_TOKEN'),
        getenv('BENTIAN_PREVIOUS_TOKEN') // Token en retirada
    ];
    $isAuthenticated = false;
    foreach ($validTokens as $expected) {
        if (!empty($expected) && hash_equals($expected, $receivedToken)) {
            $isAuthenticated = true;
            break;
        }
    }

9. Matriz de Códigos de Respuesta HTTP y Rate Limiting

El endpoint debe comunicar el resultado de cada operación utilizando códigos de estado HTTP semánticos RFC 9110:

Código HTTP Significado Técnico Comportamiento del Agente Bentian
200 OK Petición procesada con éxito. Payload JSON íntegro. Procesa pedidos o confirma el ciclo ACK satisfactoriamente.
400 Bad Request JSON malformado o campos requeridos ausentes. Registra advertencia en el log de diagnóstico y omite el registro corrupto.
401 Unauthorized Cabecera X-Bentian-Token ausente o incorrecta. Marca error de credenciales en el GUI del agente y detiene el polling.
403 Forbidden Firma HMAC inválida o timestamp expirado (>300s). Registra alerta de seguridad y sincroniza el reloj local del sistema con NTP.
429 Too Many Requests Límite de tasa excedido en el hosting (WAF o Cloudflare). Respeta la cabecera Retry-After: 60 y activa backoff exponencial suave.
500 Internal Error Error de base de datos MySQL/MariaDB en el servidor. Aplica reintentos programados sin perder ningún pedido en la cola local.

Recomendación de Rate Limiting: Configurar un umbral de 60 peticiones/minuto por IP para el agente de Bentian. Esto garantiza margen de sobra para consultas de pedidos y descargas periódicas sin disparar bloqueos de seguridad del WAF.

10. Implementación de Referencia en PHP (Plesk / cPanel)

A continuación se proporciona una plantilla de integración completa en PHP, lista para subir a httpdocs/erp-bridge-endpoint.php en Plesk o cPanel. Incluye lectura segura de token por entorno, validación en tiempo constante (hash_equals), verificación anti-replay, soporte para descarga de pedidos, flujo ACK transaccional y actualización de stock:

<?php
/**
 * ============================================================================
 * Bentian ERP Bridge — Endpoint Universal Enterprise v1.1.0
 * ============================================================================
 * Ubicación en Plesk: httpdocs/erp-bridge-endpoint.php
 * Requisitos: PHP 7.4+ con extensiones PDO MySQL / OpenSSL activas
 * ============================================================================
 */

error_reporting(0);
ini_set('display_errors', '0');

header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');

// 1. Obtención de Secretos y Configuración desde Variables de Entorno
$secretToken = getenv('BENTIAN_ENDPOINT_TOKEN') ?: 'CAMBIA_ESTE_TOKEN_SECRETO_AQUI';
$dbHost      = getenv('BENTIAN_DB_HOST') ?: '127.0.0.1';
$dbName      = getenv('BENTIAN_DB_NAME') ?: 'mi_tienda_db';
$dbUser      = getenv('BENTIAN_DB_USER') ?: 'mi_usuario_db';
$dbPass      = getenv('BENTIAN_DB_PASS') ?: 'mi_password_db';

// 2. Extracción Compatible de Cabeceras HTTP
$headers = function_exists('getallheaders') ? getallheaders() : [];
$receivedToken = $headers['X-Bentian-Token'] 
    ?? $headers['x-bentian-token'] 
    ?? ($_SERVER['HTTP_X_BENTIAN_TOKEN'] ?? '');

$receivedTimestamp = (int)($headers['X-Bentian-Timestamp'] 
    ?? $headers['x-bentian-timestamp'] 
    ?? ($_SERVER['HTTP_X_BENTIAN_TIMESTAMP'] ?? 0));

$receivedSignature = $headers['X-Bentian-Signature'] 
    ?? $headers['x-bentian-signature'] 
    ?? ($_SERVER['HTTP_X_BENTIAN_SIGNATURE'] ?? '');

// 3. Autenticación en Tiempo Constante (Prevención de Timing Attacks)
if (empty($receivedToken) || !hash_equals($secretToken, $receivedToken)) {
    http_response_code(401);
    echo json_encode(['error' => 'No autorizado: Token de Bentian no coincide o ausente']);
    exit;
}

// 4. Validación de Marca de Tiempo Anti-Replay (Ventana de 300 segundos)
if ($receivedTimestamp > 0 && abs(time() - $receivedTimestamp) > 300) {
    http_response_code(403);
    echo json_encode(['error' => 'Petición expirada: Timestamp fuera de la ventana permitida (300s)']);
    exit;
}

// 5. Conexión PDO a Base de Datos
try {
    $pdo = new PDO("mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4", $dbUser, $dbPass, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);
} catch (Exception $e) {
    http_response_code(500);
    echo json_encode(['error' => 'Error de conexión con la base de datos local']);
    exit;
}

// 6. Enrutador de Acciones
$action = $_GET['action'] ?? 'get_orders';
$rawBody = file_get_contents('php://input');
$body = json_decode($rawBody, true) ?: [];

// Acción A: Verificación de Estado (Ping)
if ($action === 'ping') {
    echo json_encode([
        'status' => 'ok',
        'success' => true,
        'service' => 'Bentian Universal Bridge',
        'version' => '1.1.0',
        'databaseConnected' => true,
        'timestamp' => time()
    ]);
    exit;
}

// Acción B: Descarga de Pedidos Pendientes con Paginación
if ($action === 'get_orders' || $action === 'pull_orders') {
    $limit = isset($_GET['limit']) ? max(1, min(200, intval($_GET['limit']))) : 50;
    $cursor = isset($_GET['cursor']) ? intval($_GET['cursor']) : 0;

    $stmt = $pdo->prepare("
        SELECT id, order_number, status, total, subtotal, tax_total, shipping_cost,
               customer_data, order_lines, payment_method, created_at
        FROM eb_orders
        WHERE status = 'PENDING' AND id > :cursor
        ORDER BY id ASC
        LIMIT :lim
    ");
    $stmt->bindValue(':cursor', $cursor, PDO::PARAM_INT);
    $stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
    $stmt->execute();
    $rows = $stmt->fetchAll();

    $orders = [];
    $lastId = $cursor;
    foreach ($rows as $r) {
        $lastId = intval($r['id']);
        $orders[] = [
            'id' => $lastId,
            'orderNumber' => $r['order_number'] ?: ('PED-' . $lastId),
            'status' => $r['status'],
            'total' => floatval($r['total']),
            'subtotal' => floatval($r['subtotal'] ?? $r['total']),
            'taxTotal' => floatval($r['tax_total'] ?? 0),
            'shippingCost' => floatval($r['shipping_cost'] ?? 0),
            'paymentMethod' => $r['payment_method'] ?? 'web',
            'createdAt' => $r['created_at'],
            'customer' => json_decode($r['customer_data'], true) ?: [],
            'lines' => json_decode($r['order_lines'], true) ?: []
        ];
    }

    echo json_encode([
        'version' => '1.1.0',
        'success' => true,
        'pagination' => [
            'limit' => $limit,
            'count' => count($orders),
            'hasMore' => count($orders) === $limit,
            'nextCursor' => count($orders) === $limit ? $lastId : null
        ],
        'orders' => $orders
    ]);
    exit;
}

// Acción C: Confirmación de Pedidos Sincronizados (Flujo ACK Individual o en Bloque)
if ($action === 'ack_orders' || $action === 'ack') {
    $confirmations = [];
    if (isset($body['confirmations']) && is_array($body['confirmations'])) {
        $confirmations = $body['confirmations'];
    } elseif (isset($body['orderId'])) {
        $confirmations = [$body];
    }

    if (empty($confirmations)) {
        http_response_code(400);
        echo json_encode(['error' => 'Estructura inválida: se esperaba "confirmations" o "orderId"']);
        exit;
    }

    $stmt = $pdo->prepare("
        UPDATE eb_orders
        SET status = 'SYNCED',
            factusol_order_number = :factNum,
            factusol_series = :factSer,
            synced_at = NOW()
        WHERE id = :id AND status = 'PENDING'
    ");

    $updated = 0;
    $pdo->beginTransaction();
    try {
        foreach ($confirmations as $c) {
            $stmt->execute([
                ':id' => intval($c['orderId'] ?? 0),
                ':factNum' => intval($c['factusolOrderNumber'] ?? 0) ?: null,
                ':factSer' => substr(strval($c['factusolSeries'] ?? '1'), 0, 5)
            ]);
            $updated += $stmt->rowCount();
        }
        $pdo->commit();
        echo json_encode(['success' => true, 'updated' => $updated]);
    } catch (Exception $e) {
        $pdo->rollBack();
        http_response_code(500);
        echo json_encode(['error' => 'Error al persistir confirmación ACK: ' . $e->getMessage()]);
    }
    exit;
}

// Acción D: Actualización de Stock en Tiempo Real desde Factusol
if ($action === 'push_stock') {
    $updates = $body['stockUpdates'] ?? [];
    if (empty($updates)) {
        http_response_code(400);
        echo json_encode(['error' => 'Parámetro stockUpdates vacío o ausente']);
        exit;
    }

    $stmt = $pdo->prepare("
        INSERT INTO eb_stock (code, stock, updated_at)
        VALUES (:code, :stock, NOW())
        ON DUPLICATE KEY UPDATE stock = :stock_up, updated_at = NOW()
    ");

    $count = 0;
    $pdo->beginTransaction();
    try {
        foreach ($updates as $u) {
            $code = trim($u['sku'] ?? $u['code'] ?? '');
            $stock = floatval($u['stock'] ?? 0);
            if (!empty($code)) {
                $stmt->execute([
                    ':code' => $code,
                    ':stock' => $stock,
                    ':stock_up' => $stock
                ]);
                $count++;
            }
        }
        $pdo->commit();
        echo json_encode(['success' => true, 'count' => $count]);
    } catch (Exception $e) {
        $pdo->rollBack();
        http_response_code(500);
        echo json_encode(['error' => 'Error al actualizar existencias: ' . $e->getMessage()]);
    }
    exit;
}

http_response_code(400);
echo json_encode(['error' => 'Acción no reconocida: ' . htmlspecialchars($action)]);

¿Tienes dudas sobre la configuración en tu empresa?

Nuestro equipo de ingeniería te asiste con tu base de datos Factusol o red local.

Contactar con Soporte