Fix: socket permissions para nginx

This commit is contained in:
2026-05-21 23:21:41 -06:00
parent b574ae1ed3
commit 28faa727b1
32 changed files with 2144 additions and 6 deletions
+189
View File
@@ -0,0 +1,189 @@
<?php
namespace App\Services;
use App\Models\Alumno;
use App\Models\Promocion;
use App\Models\WhatsappLog;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class WhatsAppService
{
private string $token;
private string $phoneId;
private string $apiUrl;
public function __construct()
{
$token = config('services.whatsapp.token');
$phoneId = config('services.whatsapp.phone_id');
if (!$token || !$phoneId) {
throw new \RuntimeException(
'WhatsApp no configurado. Agrega WHATSAPP_TOKEN y WHATSAPP_PHONE_ID en el archivo .env'
);
}
$this->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
{
$components = [];
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 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',
};
}
}