Files
Sistema-Educativo-Laravel/app/Http/Controllers/WhatsAppWebhookController.php
T
fernando 61b18df8e8 feat: WhatsApp promociones — fechas en español, status webhook y analytics inspector
- Corrige formato de fechas a español con Carbon isoFormat('D [de] MMMM')
- Webhook procesa statuses de Meta (delivered/read/failed) y actualiza pivot alumno_promocion
- Analytics: nuevas métricas de entregados/leídos, embudo de entrega, filtro por tipo
- Inspector de payload raw por log con modal y botón copiar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 15:42:26 -06:00

155 lines
5.1 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Events\NuevoMensajeWhatsApp;
use App\Models\Alumno;
use App\Models\WhatsappLog;
use App\Services\WhatsAppService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class WhatsAppWebhookController extends Controller
{
private array $optOutKeywords = ['stop', 'no', 'cancelar', 'detener', 'baja', 'salir'];
public function verify(Request $request)
{
$verifyToken = config('services.whatsapp.verify_token');
if (
$request->get('hub_mode') === 'subscribe' &&
$request->get('hub_verify_token') === $verifyToken
) {
return response($request->get('hub_challenge'), 200);
}
return response('Forbidden', 403);
}
public function handle(Request $request, WhatsAppService $whatsapp)
{
$payload = $request->all();
try {
$entry = $payload['entry'][0] ?? null;
$changes = $entry['changes'][0] ?? null;
$value = $changes['value'] ?? null;
$message = $value['messages'][0] ?? null;
// Procesar status updates (entregado/leído/fallido) de Meta
if (!empty($value['statuses'])) {
foreach ($value['statuses'] as $status) {
$this->procesarStatus($status, $payload);
}
}
if (!$message) {
return response()->json(['status' => 'ok']);
}
$telefono = $message['from'] ?? null;
$tipo = $message['type'] ?? 'text';
$alumno = $this->alumnoPorTelefono($telefono);
$alumnoId = $alumno?->id;
$logData = [
'alumno_id' => $alumnoId,
'telefono' => $telefono,
'tipo' => 'recibido',
'payload' => $payload,
];
if ($tipo === 'text') {
$texto = $message['text']['body'] ?? '';
$logData['mensaje'] = $texto;
$logData['media_type'] = null;
if ($this->esOptOut($texto)) {
$whatsapp->procesarOptOut($telefono);
$logData['tipo'] = 'opt_out';
}
} elseif (in_array($tipo, ['image', 'video', 'audio', 'document'])) {
$mediaId = $message[$tipo]['id'] ?? null;
if ($mediaId) {
$ruta = $whatsapp->descargarMedia($mediaId);
$logData['mensaje'] = $ruta;
$logData['media_type'] = $tipo;
}
}
$log = WhatsappLog::create($logData);
if ($alumnoId) {
broadcast(new NuevoMensajeWhatsApp($log));
// Marcar como no leído para el badge
WhatsappLog::where('id', $log->id)->update(['leido' => false]);
}
} catch (\Throwable $e) {
Log::error('WhatsApp webhook error', ['error' => $e->getMessage(), 'payload' => $payload]);
}
return response()->json(['status' => 'ok']);
}
private function alumnoPorTelefono(?string $telefono): ?Alumno
{
if (!$telefono) {
return null;
}
$limpio = preg_replace('/\D/', '', $telefono);
$sinCodigo = strlen($limpio) === 12 ? substr($limpio, 2) : $limpio;
return Alumno::whereHas('alumnos', function ($q) use ($limpio, $sinCodigo) {
$q->where('telefono', $limpio)->orWhere('telefono', $sinCodigo);
})->first();
}
private function esOptOut(string $texto): bool
{
$normalizado = strtolower(trim($texto));
foreach ($this->optOutKeywords as $keyword) {
if ($normalizado === $keyword || str_contains($normalizado, $keyword)) {
return true;
}
}
return false;
}
private function procesarStatus(array $status, array $payload): void
{
$mensajeId = $status['id'] ?? null;
$estadoMeta = $status['status'] ?? null; // sent, delivered, read, failed
$telefono = $status['recipient_id'] ?? null;
$alumno = $this->alumnoPorTelefono($telefono);
WhatsappLog::create([
'alumno_id' => $alumno?->id,
'telefono' => $telefono,
'tipo' => 'status_' . $estadoMeta,
'mensaje' => "Acuse de {$estadoMeta} para mensaje {$mensajeId}",
'payload' => $payload,
]);
// Actualizar pivot alumno_promocion cuando hay confirmación de entrega o lectura
if ($mensajeId && in_array($estadoMeta, ['delivered', 'read', 'failed'])) {
$nuevoStatus = match($estadoMeta) {
'delivered' => 'entregado',
'read' => 'leido',
'failed' => 'fallido',
default => $estadoMeta,
};
DB::table('alumno_promocion')
->where('whatsapp_mensaje_id', $mensajeId)
->update(['status_envio' => $nuevoStatus]);
}
}
}