token = $token; $this->phoneId = $phoneId; $this->apiUrl = "https://graph.facebook.com/v19.0/{$this->phoneId}/messages"; } public function enviarTemplate(string $telefono, string $templateName, array $parametros = [], ?string $imagenUrl = null): ?string { $components = []; if ($imagenUrl) { $components[] = [ 'type' => 'header', 'parameters' => [ [ 'type' => 'image', 'image' => ['link' => $imagenUrl], ] ], ]; } if (!empty($parametros)) { $components[] = [ 'type' => 'body', 'parameters' => array_map(fn($p) => ['type' => 'text', 'text' => $p], $parametros), ]; } $response = Http::withToken($this->token)->post($this->apiUrl, [ 'messaging_product' => 'whatsapp', 'to' => $this->normalizarTelefono($telefono), 'type' => 'template', 'template' => [ 'name' => $templateName, 'language' => ['code' => 'es_MX'], 'components' => $components, ], ]); if ($response->failed()) { Log::error('WhatsApp template error', ['telefono' => $telefono, 'response' => $response->json()]); return null; } return $response->json('messages.0.id'); } /*public function enviarPromocion(string $telefono, Promocion $promocion): string { $payload = [ 'messaging_product' => 'whatsapp', 'to' => $this->normalizarTelefono($telefono), ]; if ($promocion->imagen) { $payload['type'] = 'image'; $payload['image'] = [ 'link' => asset('storage/' . $promocion->imagen), 'caption' => "{$promocion->titulo}\n\n{$promocion->descripcion}", ]; } else { $payload['type'] = 'text'; $payload['text'] = ['body' => "{$promocion->titulo}\n\n{$promocion->descripcion}"]; } $response = Http::withToken($this->token)->post($this->apiUrl, $payload); if ($response->failed()) { Log::error('WhatsApp promocion error', [ 'telefono' => $telefono, 'status' => $response->status(), 'body' => $response->body(), ]); throw new \RuntimeException($response->body()); } return $response->json('messages.0.id'); }*/ public function enviarPromocion(string $telefono, Promocion $promocion, string $nombre = ''): string { $codigo = 'PROMO-' . strtoupper(substr(md5($promocion->id . $telefono), 0, 6)); $mensajeId = $this->enviarTemplate( $telefono, 'promocion_laravel', [ $nombre, $promocion->fecha_inicio->locale('es')->isoFormat('D [de] MMMM'), $promocion->fecha_fin->locale('es')->isoFormat('D [de] MMMM'), $codigo, ], $promocion->imagenUrl() ); if (!$mensajeId) { throw new \RuntimeException('No se pudo enviar la plantilla'); } return $mensajeId; } public function procesarOptOut(string $telefono): void { $normalizado = $this->normalizarTelefono($telefono); Alumno::whereHas('alumnos', function ($q) use ($normalizado) { $q->where('telefono', $normalizado) ->orWhere('telefono', ltrim($normalizado, '52')); })->each(function (Alumno $alumno) { $alumno->update([ 'opt_out' => true, 'opt_out_at' => now(), ]); }); WhatsappLog::create([ 'telefono' => $normalizado, 'tipo' => 'opt_out', 'mensaje' => 'Usuario solicitó opt-out', ]); } public function descargarMedia(string $mediaId): ?string { $infoResponse = Http::withToken($this->token) ->get("https://graph.facebook.com/v19.0/{$mediaId}"); if ($infoResponse->failed()) { Log::error('WhatsApp media info error', ['mediaId' => $mediaId]); return null; } $mediaUrl = $infoResponse->json('url'); $mimeType = $infoResponse->json('mime_type', 'application/octet-stream'); $extension = $this->extensionFromMime($mimeType); $fileResponse = Http::withToken($this->token)->get($mediaUrl); if ($fileResponse->failed()) { return null; } $filename = 'whatsapp_media/' . $mediaId . '.' . $extension; Storage::disk('public')->put($filename, $fileResponse->body()); return $filename; } public function enviarTexto(string $telefono, string $texto): ?string { $response = Http::withToken($this->token)->post($this->apiUrl, [ 'messaging_product' => 'whatsapp', 'to' => $this->normalizarTelefono($telefono), 'type' => 'text', 'text' => ['body' => $texto], ]); if ($response->failed()) { return null; } return $response->json('messages.0.id'); } private function normalizarTelefono(string $telefono): string { $limpio = preg_replace('/\D/', '', $telefono); // Si ya tiene el prefijo 52 (12 dígitos), quítalo para re-agregarlo limpio if (str_starts_with($limpio, '52') && strlen($limpio) === 12) { $limpio = substr($limpio, 2); } // Siempre anteponer el código de país México return '52' . $limpio; } private function extensionFromMime(string $mimeType): string { return match (true) { str_contains($mimeType, 'jpeg') => 'jpg', str_contains($mimeType, 'png') => 'png', str_contains($mimeType, 'gif') => 'gif', str_contains($mimeType, 'webp') => 'webp', str_contains($mimeType, 'mp4') => 'mp4', str_contains($mimeType, 'ogg') => 'ogg', str_contains($mimeType, 'mpeg') => 'mp3', str_contains($mimeType, 'pdf') => 'pdf', str_contains($mimeType, 'msword') => 'doc', str_contains($mimeType, 'spreadsheet')=> 'xlsx', default => 'bin', }; } }