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; 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; } }