Modificación de Mayusculas en clase documneto tipo

This commit is contained in:
2026-06-16 15:14:48 -06:00
13 changed files with 369 additions and 177 deletions
@@ -37,12 +37,21 @@ class WhatsAppAnalyticsController extends Controller
$query->where('created_at', '<=', $request->fecha_hasta . ' 23:59:59');
}
$enviados = (clone $query)->where('tipo', 'enviado')->count();
$fallidos = (clone $query)->where('tipo', 'error')->count();
$optOuts = (clone $query)->where('tipo', 'opt_out')->count();
$recibidos = (clone $query)->where('tipo', 'recibido')->count();
// Métricas siempre sobre todos los tipos (sin filtro de tipo)
$enviados = (clone $query)->where('tipo', 'enviado')->count();
$fallidos = (clone $query)->where('tipo', 'error')->count();
$optOuts = (clone $query)->where('tipo', 'opt_out')->count();
$recibidos = (clone $query)->where('tipo', 'recibido')->count();
$entregados = (clone $query)->where('tipo', 'status_delivered')->count();
$leidos = (clone $query)->where('tipo', 'status_read')->count();
$logsRecientes = (clone $query)
// El filtro de tipo solo aplica al listado de logs
$logsQuery = clone $query;
if ($request->filled('tipo')) {
$logsQuery->where('tipo', $request->tipo);
}
$logsRecientes = $logsQuery
->with('alumno.alumnos')
->latest('created_at')
->paginate(20);
@@ -52,7 +61,7 @@ class WhatsAppAnalyticsController extends Controller
})->latest()->get();
return view('promocion.analytics', compact(
'enviados', 'fallidos', 'optOuts', 'recibidos',
'enviados', 'fallidos', 'optOuts', 'recibidos', 'entregados', 'leidos',
'logsRecientes', 'planteles', 'promociones'
));
}
@@ -2,115 +2,60 @@
namespace App\Http\Controllers;
use App\Events\NuevoMensajeWhatsApp;
use App\Models\Alumno;
use App\Models\WhatsappLog;
use App\Services\WhatsAppService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Response;
class WhatsAppWebhookController extends Controller
{
private array $optOutKeywords = ['stop', 'no', 'cancelar', 'detener', 'baja', 'salir'];
public function verify(Request $request)
public function verify(Request $request): Response
{
$verifyToken = config('services.whatsapp.verify_token');
if (
$request->get('hub_mode') === 'subscribe' &&
$request->get('hub_verify_token') === $verifyToken
$request->query('hub_mode') === 'subscribe' &&
$request->query('hub_verify_token') === $verifyToken
) {
return response($request->get('hub_challenge'), 200);
return response($request->query('hub_challenge'), 200);
}
return response('Forbidden', 403);
}
public function handle(Request $request, WhatsAppService $whatsapp)
public function handle(Request $request, WhatsAppService $whatsapp): JsonResponse
{
$payload = $request->all();
$payload = $request->all();
$value = data_get($payload, 'entry.0.changes.0.value', []);
$messages = data_get($value, 'messages', []);
$statuses = data_get($value, 'statuses', []);
try {
$entry = $payload['entry'][0] ?? null;
$changes = $entry['changes'][0] ?? null;
$value = $changes['value'] ?? null;
$message = $value['messages'][0] ?? null;
foreach ($messages as $message) {
$telefono = data_get($message, 'from');
$texto = data_get($message, 'text.body', '');
if (!$message) {
return response()->json(['status' => 'ok']);
WhatsappLog::create([
'telefono' => $telefono,
'tipo' => 'recibido',
'mensaje' => $texto,
'whatsapp_mensaje_id' => data_get($message, 'id'),
'payload' => $payload,
]);
if (preg_match('/\b(STOP|cancelar)\b/i', $texto)) {
$whatsapp->procesarOptOut($telefono);
}
}
$telefono = $message['from'] ?? null;
$tipo = $message['type'] ?? 'text';
$alumno = $this->alumnoPorTelefono($telefono);
$alumnoId = $alumno?->id;
foreach ($statuses as $status) {
$mensajeId = data_get($status, 'id');
$statusEnvio = data_get($status, 'status');
$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]);
WhatsappLog::where('whatsapp_mensaje_id', $mensajeId)
->update(['status_envio' => $statusEnvio]);
}
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;
}
}
+2 -1
View File
@@ -48,7 +48,8 @@ class EnviarPromocionJob implements ShouldQueue
}
try {
$mensajeId = $whatsapp->enviarPromocion($telefono, $this->promocion);
$nombre = $this->alumno->alumnos()->first()?->name ?? 'estudiante';
$mensajeId = $whatsapp->enviarPromocion($telefono, $this->promocion, $nombre);
} catch (\RuntimeException $e) {
WhatsappLog::create([
'alumno_id' => $this->alumno->id,
+7 -1
View File
@@ -34,6 +34,12 @@ class Promocion extends Model
public function imagenUrl(): ?string
{
return $this->imagen ? asset('storage/' . $this->imagen) : null;
if (!$this->imagen) return null;
if (str_starts_with($this->imagen, 'http')) {
return $this->imagen;
}
return asset('storage/' . $this->imagen);
}
}
+84 -28
View File
@@ -28,40 +28,71 @@ class WhatsAppService
$this->token = $token;
$this->phoneId = $phoneId;
$this->apiUrl = "https://graph.facebook.com/v19.0/{$this->phoneId}/messages";
$this->apiUrl = "https://graph.facebook.com/v25.0/{$this->phoneId}/messages";
}
public function enviarTemplate(string $telefono, string $templateName, array $parametros = []): ?string
{
$components = [];
public function enviarTemplate(string $telefono, string $templateName, array $parametros = [], ?string $imagenUrl = null): ?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 ($imagenUrl) {
$components[] = [
'type' => 'header',
'parameters' => [
[
'type' => 'image',
'image' => ['link' => $imagenUrl],
]
],
]);
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
if (!empty($parametros)) {
$components[] = [
'type' => 'body',
'parameters' => array_map(fn($nombre, $valor) => [
'type' => 'text',
'text' => (string) $valor,
'parameter_name' => $nombre,
], array_keys($parametros), array_values($parametros)),
];
}
Log::info('WhatsApp payload', [
'url' => $this->apiUrl,
'body' => [
'messaging_product' => 'whatsapp',
'to' => $this->normalizarTelefono($telefono),
'type' => 'template',
'template' => [
'name' => $templateName,
'language' => ['code' => 'es_MX'],
'components' => $components,
],
]
]);
$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(), 'body' => $response->body()]);
return null;
}
return $response->json('messages.0.id');
}
/*public function enviarPromocion(string $telefono, Promocion $promocion): string
{
$payload = [
'messaging_product' => 'whatsapp',
@@ -92,8 +123,33 @@ class WhatsAppService
}
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' => $nombre ?: 'estudiante',
'fecha_inicio' => $promocion->fecha_inicio->locale('es')->isoFormat('D [de] MMMM'),
'fecha_fin' => $promocion->fecha_fin->locale('es')->isoFormat('D [de] MMMM'),
'codigo' => $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);