feat: URL var config per endpoint + smart date handling in api_report
- Admin: each endpoint with {vars} shows a vars config section; per-var
mode is "ask user" (bot prompts in WhatsApp) or "fixed" (preset: today,
month_start, month_end, last_7, last_30, year_start, or literal)
- Bot: resolves fixed vars before calling endpoint; queues "ask" vars and
collects them sequentially from the user before executing the report
- date_mode now uses fecha_desde/fecha_hasta (ERP expected names); added
current_month mode
- JSON responses from ERP (sin_datos etc.) shown as text, not uploaded as file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
fb28140e23
commit
ff9ff67445
+131
-7
@@ -59,6 +59,11 @@ class NormalBot
|
||||
return self::handleCapInput($meta, $input, $context, $company, $ctxId);
|
||||
}
|
||||
|
||||
// 2d. api_vars: collecting URL variables before executing an api_report endpoint
|
||||
if (!empty($meta['__api_vars'])) {
|
||||
return self::handleApiVarsInput($meta, $input, $context, $company, $ctxId, $flows, $menus);
|
||||
}
|
||||
|
||||
// 3. Active node — resume conversation
|
||||
$sentinels = ['collecting', '__greeted'];
|
||||
if ($currentNode !== null && !in_array($currentNode, $sentinels, true) && isset($flows[$currentNode])) {
|
||||
@@ -178,8 +183,14 @@ class NormalBot
|
||||
// Reemplaza {variable} en la URL con valores del metadata acumulado
|
||||
private static function substituteUrlVars(string $url, array $meta): string
|
||||
{
|
||||
// Priority: __ep_vars_combined (vars collected/resolved for this endpoint call)
|
||||
$epVars = $meta['__ep_vars_combined'] ?? [];
|
||||
foreach ($epVars as $key => $value) {
|
||||
$url = str_replace('{' . $key . '}', urlencode((string)$value), $url);
|
||||
}
|
||||
// Then the rest of meta groups
|
||||
foreach ($meta as $group => $fields) {
|
||||
if (!is_array($fields)) continue;
|
||||
if (!is_array($fields) || str_starts_with($group, '__')) continue;
|
||||
foreach ($fields as $key => $value) {
|
||||
$url = str_replace('{' . $key . '}', urlencode((string)$value), $url);
|
||||
}
|
||||
@@ -620,6 +631,57 @@ class NormalBot
|
||||
};
|
||||
}
|
||||
|
||||
// ── Resolves fixed-mode var values ───────────────────────────────────────
|
||||
private static function resolveFixedVar(string $value): string
|
||||
{
|
||||
return match ($value) {
|
||||
'current_date' => date('Y-m-d'),
|
||||
'month_start' => date('Y-m-01'),
|
||||
'month_end' => date('Y-m-t'),
|
||||
'last_7_start' => date('Y-m-d', strtotime('-7 days')),
|
||||
'last_30_start' => date('Y-m-d', strtotime('-30 days')),
|
||||
'year_start' => date('Y-01-01'),
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Handles sequential collection of URL vars before calling endpoint ───
|
||||
private static function handleApiVarsInput(array $meta, string $input, array $context, array $company, int $ctxId, array $flows, array $menus): ?array
|
||||
{
|
||||
$nav = "\n\nEscribe *menu* para ver opciones o *salir* para terminar.";
|
||||
$state = $meta['__api_vars'];
|
||||
|
||||
$pending = $state['pending'] ?? [];
|
||||
$collected = $state['collected'] ?? [];
|
||||
$epKey = $state['endpoint_key'] ?? '';
|
||||
$epParams = $state['ep_params'] ?? [];
|
||||
|
||||
// Save the user's answer for the current question
|
||||
if (!empty($pending)) {
|
||||
$current = array_shift($pending);
|
||||
$collected[$current['key']] = trim($input);
|
||||
$state['pending'] = $pending;
|
||||
$state['collected'] = $collected;
|
||||
}
|
||||
|
||||
// If more questions remain, ask the next one
|
||||
if (!empty($state['pending'])) {
|
||||
$next = $state['pending'][0];
|
||||
$meta['__api_vars'] = $state;
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
return self::sendText($next['prompt'] . $nav, $context['from'], $company);
|
||||
}
|
||||
|
||||
// All vars collected — inject into meta and execute
|
||||
unset($meta['__api_vars']);
|
||||
foreach ($collected as $k => $v) {
|
||||
$meta['__ep_vars'][$k] = $v;
|
||||
}
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
|
||||
return self::executeApiReport(array_merge($epParams, ['endpoint_key' => $epKey]), $context, $company, $ctxId);
|
||||
}
|
||||
|
||||
private static function executeApiReport(array $params, array $context, array $company, int $ctxId): ?array
|
||||
{
|
||||
$endpointKey = $params['endpoint_key'] ?? '';
|
||||
@@ -630,7 +692,7 @@ class NormalBot
|
||||
return self::sendText('Error: endpoint no configurado.' . $nav, $context['from'], $company);
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("SELECT url, method FROM company_endpoints WHERE company_id = ? AND endpoint_key = ? AND is_active = 1 LIMIT 1");
|
||||
$stmt = db()->prepare("SELECT url, method, params FROM company_endpoints WHERE company_id = ? AND endpoint_key = ? AND is_active = 1 LIMIT 1");
|
||||
$stmt->execute([(int)$company['id'], $endpointKey]);
|
||||
$ep = $stmt->fetch();
|
||||
|
||||
@@ -640,18 +702,72 @@ class NormalBot
|
||||
return self::sendText('El informe solicitado no está configurado. Contacta al administrador.' . $nav, $context['from'], $company);
|
||||
}
|
||||
|
||||
$meta = ConversationContext::getMetadata($ctxId);
|
||||
$url = self::substituteUrlVars($ep['url'], $meta); // reemplaza {var} con valores del formulario
|
||||
// ── Resolve URL vars from params config ──────────────────────────────
|
||||
$varConfigs = json_decode($ep['params'] ?? '[]', true) ?: [];
|
||||
$meta = ConversationContext::getMetadata($ctxId);
|
||||
|
||||
// Inject any previously collected vars into meta for substituteUrlVars
|
||||
$epVars = $meta['__ep_vars'] ?? [];
|
||||
if (!empty($epVars)) {
|
||||
$meta['__ep_vars_flat'] = $epVars;
|
||||
}
|
||||
|
||||
$askVars = [];
|
||||
foreach ($varConfigs as $vc) {
|
||||
$key = $vc['key'] ?? '';
|
||||
$mode = $vc['mode'] ?? 'ask';
|
||||
if ($key === '') continue;
|
||||
|
||||
if ($mode === 'fixed') {
|
||||
// Inject fixed value directly into meta for substituteUrlVars
|
||||
$meta['__ep_vars_flat'][$key] = self::resolveFixedVar($vc['value'] ?? '');
|
||||
} elseif ($mode === 'ask') {
|
||||
// Only ask if not already collected
|
||||
if (!isset($epVars[$key])) {
|
||||
$askVars[] = ['key' => $key, 'prompt' => $vc['prompt'] ?? "Ingresa el valor para {$key}:"];
|
||||
} else {
|
||||
$meta['__ep_vars_flat'][$key] = $epVars[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are vars to ask, start collection flow
|
||||
if (!empty($askVars)) {
|
||||
$first = array_shift($askVars);
|
||||
$meta['__api_vars'] = [
|
||||
'endpoint_key' => $endpointKey,
|
||||
'ep_params' => $params,
|
||||
'pending' => $askVars,
|
||||
'collected' => [],
|
||||
];
|
||||
// Clean collected vars so they don't bleed into the next call
|
||||
unset($meta['__ep_vars']);
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
return self::sendText($first['prompt'] . $nav, $context['from'], $company);
|
||||
}
|
||||
|
||||
// Flatten collected vars into meta for substituteUrlVars
|
||||
if (!empty($meta['__ep_vars_flat'])) {
|
||||
$meta['__ep_vars_combined'] = $meta['__ep_vars_flat'];
|
||||
}
|
||||
unset($meta['__ep_vars'], $meta['__ep_vars_flat']);
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
|
||||
$url = self::substituteUrlVars($ep['url'], array_merge($meta, ['__ep_vars_combined' => $meta['__ep_vars_combined'] ?? []]));
|
||||
$method = strtoupper($ep['method'] ?? 'GET');
|
||||
$apiKey = $company['api_key'] ?? '';
|
||||
|
||||
$extraQuery = $params['query'] ?? [];
|
||||
$dateMode = $params['date_mode'] ?? '';
|
||||
if ($dateMode === 'today') {
|
||||
$extraQuery['fecha'] = date('Y-m-d');
|
||||
$extraQuery['fecha_desde'] = date('Y-m-d');
|
||||
$extraQuery['fecha_hasta'] = date('Y-m-d');
|
||||
} elseif ($dateMode === 'last_30') {
|
||||
$extraQuery['fecha_inicio'] = date('Y-m-d', strtotime('-30 days'));
|
||||
$extraQuery['fecha_fin'] = date('Y-m-d');
|
||||
$extraQuery['fecha_desde'] = date('Y-m-d', strtotime('-30 days'));
|
||||
$extraQuery['fecha_hasta'] = date('Y-m-d');
|
||||
} elseif ($dateMode === 'current_month') {
|
||||
$extraQuery['fecha_desde'] = date('Y-m-01');
|
||||
$extraQuery['fecha_hasta'] = date('Y-m-d');
|
||||
}
|
||||
$extraQuery['telefono'] = $context['from'];
|
||||
$extraQuery['nombre'] = $context['name'] ?? '';
|
||||
@@ -693,6 +809,14 @@ class NormalBot
|
||||
return self::sendText('Error al obtener el reporte. Intenta de nuevo.' . $nav, $context['from'], $company);
|
||||
}
|
||||
|
||||
// Si el ERP devuelve JSON (ej: sin_datos), informar al usuario en texto
|
||||
if (str_starts_with($contentType ?? '', 'application/json')) {
|
||||
$json = json_decode($content, true);
|
||||
$msg = $json['mensaje'] ?? ($json['message'] ?? 'No hay datos disponibles para el período solicitado.');
|
||||
ConversationContext::reset($ctxId);
|
||||
return self::sendText('ℹ️ ' . $msg . $nav, $context['from'], $company);
|
||||
}
|
||||
|
||||
$mimeMap = [
|
||||
'application/pdf' => ['pdf', 'application/pdf'],
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
||||
|
||||
Reference in New Issue
Block a user