From 8e7873f2dcf2fdd214b7715d4f757aff02f7738b Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Fri, 14 Nov 2025 16:31:15 -0600 Subject: [PATCH 1/3] ADD: Command para ver vencimiento de paquetes --- .../Commands/CheckExpiringPackages.php | 154 ++++++++++++++++++ app/Models/SimCard.php | 18 ++ routes/console.php | 6 + 3 files changed, 178 insertions(+) create mode 100644 app/Console/Commands/CheckExpiringPackages.php diff --git a/app/Console/Commands/CheckExpiringPackages.php b/app/Console/Commands/CheckExpiringPackages.php new file mode 100644 index 0000000..64afa8d --- /dev/null +++ b/app/Console/Commands/CheckExpiringPackages.php @@ -0,0 +1,154 @@ +info('Verificando paquetes por vencer...'); + $this->newLine(); + + // Obtener configuración + $notificationDays = array_map('intval', explode(',', $this->option('days'))); + $testMode = $this->option('test'); + + if ($testMode) { + $this->warn('TEST'); + $this->newLine(); + } + + // Obtener paquetes activos con sus relaciones + $activePackages = PackSim::with(['package', 'simCard.clientSims.client']) + ->where('is_active', true) + ->whereNotNull('activated_at') + ->get(); + + $this->info("paquetes activos: {$activePackages->count()}"); + $this->newLine(); + + $notificationsSent = 0; + $packagesChecked = 0; + + if ($this->option('show-all')) { + $this->showAllPackages($activePackages); + return Command::SUCCESS; + } + + foreach ($activePackages as $packSim) { + $packagesChecked++; + + // Calcular fecha de vencimiento + $activatedAt = Carbon::parse($packSim->activated_at); + $expirationDate = $activatedAt->copy()->addDays($packSim->package->period); + + // Días restante + $daysRemaining = (int) now()->diffInDays($expirationDate, false); + + // Verificar si debe notificar + if (in_array($daysRemaining, $notificationDays)) { + // Obtener cliente activo + $clientRelation = $packSim->simCard->activeClient()->first(); + $client = $clientRelation; + + if (!$client) { + $this->warn("SIM {$packSim->simCard->iccid} sin cliente activo"); + continue; + } + + // Mostrar información + if ($testMode) { + $this->displayNotificationDetails($client, $packSim, $daysRemaining); + } else { + // notificacion real + $this->sendWhatsAppNotification($client, $packSim, $daysRemaining); + } + + $notificationsSent++; + } + } + + $this->newLine(); + $this->info('Resumen'); + $this->table( + ['Métrica', 'Valor'], + [ + ['Paquetes revisados', $packagesChecked], + ['Notificaciones enviadas', $notificationsSent], + ['Modo', $testMode ? 'TEST' : 'PRODUCCIÓN'], + ] + ); + + return Command::SUCCESS; + } + + private function displayNotificationDetails($client, $packSim, $daysRemaining) + { + $this->line('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + $this->info(" NOTIFICACIÓN A ENVIAR:"); + $this->line(" Cliente: {$client->full_name}"); + $this->line(" Teléfono: {$client->phone}"); + $this->line(" Email: {$client->email}"); + $this->line(" Paquete: {$packSim->package->name}"); + $this->line(" SIM (ICCID): {$packSim->simCard->iccid}"); + $this->line(" Activado: " . Carbon::parse($packSim->activated_at)->format('d/m/Y')); + $this->line(" Vence en: {$daysRemaining} día(s)"); + $this->line('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + $this->newLine(); + } + + + private function showAllPackages($packages) + { + $this->info('Mostrando todos los paquetes activos con sus fechas de vencimiento:'); + $this->newLine(); + + $data = []; + + foreach ($packages as $packSim) { + $activatedAt = Carbon::parse($packSim->activated_at); + $expirationDate = $activatedAt->copy()->addDays($packSim->package->period); + $daysRemaining = (int) now()->diffInDays($expirationDate, false); + + $clientRelation = $packSim->simCard->activeClient()->first(); + $clientName = $clientRelation ? $clientRelation->full_name : 'Sin cliente activo'; + + $data[] = [ + 'Cliente' => $clientName, + 'Paquete' => $packSim->package->name, + 'SIM (ICCID)' => $packSim->simCard->iccid, + 'Activado' => $activatedAt->format('d/m/Y'), + 'Vence el' => $expirationDate->format('d/m/Y'), + 'Días restantes' => $daysRemaining, + ]; + } + + $this->table( + ['Cliente', 'Paquete', 'SIM (ICCID)', 'Activado', 'Vence el', 'Días restantes'], + $data + ); + } +} diff --git a/app/Models/SimCard.php b/app/Models/SimCard.php index 1eacd84..c2db10e 100644 --- a/app/Models/SimCard.php +++ b/app/Models/SimCard.php @@ -57,4 +57,22 @@ public function activePackage() ->withTimestamps() ->limit(1); } + + public function clientSims() + { + return $this->hasMany(ClientSim::class, 'sim_card_id'); + } + + public function activeClient() + { + return $this->belongsToMany( + Client::class, + 'client_sims', + 'sim_card_id', + 'client_id' + )->wherePivot('is_active', true) + ->withPivot('assigned_at', 'released_at', 'is_active') + ->withTimestamps() + ->limit(1); + } } diff --git a/routes/console.php b/routes/console.php index 0b326d0..51419df 100644 --- a/routes/console.php +++ b/routes/console.php @@ -8,3 +8,9 @@ } Schedule::call(new DeleteResetPasswords)->hourly(); + +Schedule::command('packages:check-expiring') + ->dailyAt('09:00') + ->timezone('America/Mexico_City') + ->withoutOverlapping() + ->appendOutputTo(storage_path('logs/packages-expiring.log')); -- 2.45.2 From 1523cd408d02a143b7bc37220a1c73549c44b3ce Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Fri, 14 Nov 2025 22:57:50 -0600 Subject: [PATCH 2/3] ADD: mostrar paquetes por expirar --- .../Commands/CheckExpiringPackages.php | 76 +++++++++-- .../Netbien/PackagesController.php | 118 ++++++++++++++++++ routes/api.php | 1 + 3 files changed, 187 insertions(+), 8 deletions(-) diff --git a/app/Console/Commands/CheckExpiringPackages.php b/app/Console/Commands/CheckExpiringPackages.php index 64afa8d..1905dff 100644 --- a/app/Console/Commands/CheckExpiringPackages.php +++ b/app/Console/Commands/CheckExpiringPackages.php @@ -15,7 +15,7 @@ class CheckExpiringPackages extends Command * Nombre del comando */ protected $signature = 'packages:check-expiring - {--days=7,3,1 : Días de anticipación para notificar} + {--days= : Días de anticipación para notificar} {--test : Modo de prueba } {--show-all : Mostrar todos los paquetes con sus fechas de vencimiento}'; @@ -33,7 +33,7 @@ public function handle() $this->newLine(); // Obtener configuración - $notificationDays = array_map('intval', explode(',', $this->option('days'))); + $customDays = $this->option('days'); $testMode = $this->option('test'); if ($testMode) { @@ -41,6 +41,16 @@ public function handle() $this->newLine(); } + if ($customDays !== null) { + $this->info("Usando días personalizados: {$customDays}"); + } else { + $this->info("Periodo de paquete:"); + $this->line(" • Paquetes de 30 días → Notificar 7 días antes"); + $this->line(" • Paquetes de 15 días → Notificar 3 días antes"); + $this->line(" • Paquetes de 7 días → Notificar 2 días antes"); + } + $this->newLine(); + // Obtener paquetes activos con sus relaciones $activePackages = PackSim::with(['package', 'simCard.clientSims.client']) ->where('is_active', true) @@ -65,11 +75,17 @@ public function handle() $activatedAt = Carbon::parse($packSim->activated_at); $expirationDate = $activatedAt->copy()->addDays($packSim->package->period); - // Días restante + // Días restantes $daysRemaining = (int) now()->diffInDays($expirationDate, false); - // Verificar si debe notificar - if (in_array($daysRemaining, $notificationDays)) { + // Determinar si debe notificar según el periodo del paquete + $shouldNotify = $this->shouldNotifyForPackage( + $packSim->package->period, + $daysRemaining, + $customDays + ); + + if ($shouldNotify) { // Obtener cliente activo $clientRelation = $packSim->simCard->activeClient()->first(); $client = $clientRelation; @@ -105,17 +121,61 @@ public function handle() return Command::SUCCESS; } + /** + * Determina si se debe notificar según el periodo del paquete y días restantes + */ + private function shouldNotifyForPackage($packagePeriod, $daysRemaining, $customDays = null) + { + // Si se especificaron días personalizados + if ($customDays !== null) { + $notificationDays = array_map('intval', explode(',', $customDays)); + return in_array($daysRemaining, $notificationDays); + } + + // Lógica según el periodo del paquete + switch ($packagePeriod) { + case 30: + // Paquetes de 30 días: notificar a los 7 días + return $daysRemaining === 7; + + case 15: + // Paquetes de 15 días: notificar a los 3 días + return $daysRemaining === 3; + + case 7: + // Paquetes de 7 días: notificar a los 2 días + return $daysRemaining === 2; + + default: + // Para otros periodos, no notificar (o puedes ajustar la lógica) + return false; + } + } + + /** + * Envía notificación por WhatsApp (temporal: solo muestra en consola) + */ + private function sendWhatsAppNotification($client, $packSim, $daysRemaining) + { + $this->warn('PRODUCCIÓN: Aquí se enviaría la notificación real por WhatsApp'); + $this->displayNotificationDetails($client, $packSim, $daysRemaining); + } + private function displayNotificationDetails($client, $packSim, $daysRemaining) { + $activatedAt = Carbon::parse($packSim->activated_at); + $expirationDate = $activatedAt->copy()->addDays($packSim->package->period); + $this->line('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); $this->info(" NOTIFICACIÓN A ENVIAR:"); $this->line(" Cliente: {$client->full_name}"); $this->line(" Teléfono: {$client->phone}"); $this->line(" Email: {$client->email}"); - $this->line(" Paquete: {$packSim->package->name}"); + $this->line(" Paquete: {$packSim->package->name} ({$packSim->package->period} días)"); $this->line(" SIM (ICCID): {$packSim->simCard->iccid}"); - $this->line(" Activado: " . Carbon::parse($packSim->activated_at)->format('d/m/Y')); - $this->line(" Vence en: {$daysRemaining} día(s)"); + $this->line(" Activado: " . $activatedAt->format('d/m/Y')); + $this->line(" Vence el: " . $expirationDate->format('d/m/Y')); + $this->line(" Días restantes: {$daysRemaining} día(s)"); $this->line('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); $this->newLine(); } diff --git a/app/Http/Controllers/Netbien/PackagesController.php b/app/Http/Controllers/Netbien/PackagesController.php index 8bbd1d0..fa57d96 100644 --- a/app/Http/Controllers/Netbien/PackagesController.php +++ b/app/Http/Controllers/Netbien/PackagesController.php @@ -7,6 +7,9 @@ use App\Http\Requests\Netbien\PackagesStoreRequest; use App\Http\Requests\Netbien\PackagesUpdateRequest; use App\Models\Packages; +use App\Models\PackSim; +use Carbon\Carbon; +use Illuminate\Http\Request; use Notsoweb\ApiResponse\Enums\ApiResponse; /** @@ -56,4 +59,119 @@ public function destroy(Packages $package) return ApiResponse::NO_CONTENT->response(); } + + /** + * Obtiene los paquetes que están por vencer + * + */ + public function expiring(Request $request) + { + // Parámetros de consulta + $customDays = $request->query('days'); + $period = $request->query('period'); + $withClientOnly = $request->boolean('with_client', false); + + // Obtener paquetes activos con sus relaciones + $query = PackSim::with(['package', 'simCard.activeClient']) + ->where('is_active', true) + ->whereNotNull('activated_at'); + + // Filtrar por periodo si se especifica + if ($period !== null) { + $query->whereHas('package', function ($q) use ($period) { + $q->where('period', $period); + }); + } + + $activePackages = $query->get(); + + // Filtrar y calcular días restantes + $expiringPackages = $activePackages + ->map(function ($packSim) use ($customDays, $withClientOnly) { + // Calcular fecha de vencimiento + $activatedAt = Carbon::parse($packSim->activated_at); + $expirationDate = $activatedAt->copy()->addDays($packSim->package->period); + $daysRemaining = (int) now()->diffInDays($expirationDate, false); + + // Determinar si debe incluirse + $shouldInclude = $this->shouldIncludePackage( + $packSim->package->period, + $daysRemaining, + $customDays + ); + + if (!$shouldInclude) { + return null; + } + + // Obtener cliente + $client = $packSim->simCard->activeClient()->first(); + + // Filtrar por cliente si se requiere + if ($withClientOnly && !$client) { + return null; + } + + // Formatear respuesta + return [ + 'id' => $packSim->id, + 'sim_card' => [ + 'id' => $packSim->simCard->id, + 'iccid' => $packSim->simCard->iccid, + 'msisdn' => $packSim->simCard->msisdn, + ], + 'package' => [ + 'id' => $packSim->package->id, + 'name' => $packSim->package->name, + 'period' => $packSim->package->period, + ], + 'client' => $client ? [ + 'id' => $client->id, + 'full_name' => $client->full_name, + 'phone' => $client->phone, + 'email' => $client->email, + ] : null, + 'activated_at' => $activatedAt->format('Y-m-d'), + 'expires_at' => $expirationDate->format('Y-m-d'), + 'days_remaining' => $daysRemaining, + ]; + }) + ->filter() + ->values(); + + return ApiResponse::OK->response([ + 'packages' => $expiringPackages, + ]); + } + + /** + * Determina si un paquete debe incluirse según el periodo y días restantes + */ + private function shouldIncludePackage(int $packagePeriod, int $daysRemaining, ?string $customDays = null): bool + { + // Si se especificaron días personalizados, usarlos + if ($customDays !== null) { + $notificationDays = array_map('intval', explode(',', $customDays)); + return in_array($daysRemaining, $notificationDays); + } + + // Lógica automática según el periodo del paquete + switch ($packagePeriod) { + case 30: + // Paquetes de 30 días: incluir si quedan 7 días o menos + return $daysRemaining <= 7 && $daysRemaining >= 0; + + case 15: + // Paquetes de 15 días: incluir si quedan 3 días o menos + return $daysRemaining <= 3 && $daysRemaining >= 0; + + case 7: + // Paquetes de 7 días: incluir si quedan 2 días o menos + return $daysRemaining <= 2 && $daysRemaining >= 0; + + default: + // Para otros periodos, incluir si quedan 7 días o menos + return $daysRemaining <= 7 && $daysRemaining >= 0; + } + } } diff --git a/routes/api.php b/routes/api.php index e1fe90b..e41d042 100644 --- a/routes/api.php +++ b/routes/api.php @@ -27,6 +27,7 @@ Route::post('import', [SimCardController::class, 'import']); Route::resource('sim-cards', SimCardController::class); + Route::get('packages/expiring', [PackagesController::class, 'expiring']); Route::resource('packages', PackagesController::class); Route::get('clients/search', [ClientController::class, 'search']); -- 2.45.2 From 1ae0dd6b0c914bad2002a608454be56c774979c3 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Tue, 18 Nov 2025 23:44:21 -0600 Subject: [PATCH 3/3] =?UTF-8?q?FIX:=20Ajustar=20c=C3=A1lculo=20de=20d?= =?UTF-8?q?=C3=ADas=20restantes=20para=20paquetes=20en=20el=20command=20y?= =?UTF-8?q?=20controlador?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Commands/CheckExpiringPackages.php | 66 +++++++------------ .../Netbien/PackagesController.php | 13 +++- database/seeders/DevSeeder.php | 4 +- routes/api.php | 2 +- 4 files changed, 39 insertions(+), 46 deletions(-) diff --git a/app/Console/Commands/CheckExpiringPackages.php b/app/Console/Commands/CheckExpiringPackages.php index 1905dff..12b6d2b 100644 --- a/app/Console/Commands/CheckExpiringPackages.php +++ b/app/Console/Commands/CheckExpiringPackages.php @@ -6,38 +6,25 @@ use Carbon\Carbon; use Illuminate\Console\Command; -/** - * - */ class CheckExpiringPackages extends Command { - /** - * Nombre del comando - */ protected $signature = 'packages:check-expiring {--days= : Días de anticipación para notificar} {--test : Modo de prueba } {--show-all : Mostrar todos los paquetes con sus fechas de vencimiento}'; - /** - * La descripción del comando - */ protected $description = 'Verificar paquetes por vencer y enviar notificaciones a los usuarios'; - /** - * Ejecutar comando - */ public function handle() { $this->info('Verificando paquetes por vencer...'); $this->newLine(); - // Obtener configuración $customDays = $this->option('days'); $testMode = $this->option('test'); if ($testMode) { - $this->warn('TEST'); + $this->warn('MODO TEST'); $this->newLine(); } @@ -51,13 +38,12 @@ public function handle() } $this->newLine(); - // Obtener paquetes activos con sus relaciones $activePackages = PackSim::with(['package', 'simCard.clientSims.client']) ->where('is_active', true) ->whereNotNull('activated_at') ->get(); - $this->info("paquetes activos: {$activePackages->count()}"); + $this->info("Paquetes activos: {$activePackages->count()}"); $this->newLine(); $notificationsSent = 0; @@ -68,17 +54,24 @@ public function handle() return Command::SUCCESS; } + $now = Carbon::now()->startOfDay(); + foreach ($activePackages as $packSim) { $packagesChecked++; - // Calcular fecha de vencimiento $activatedAt = Carbon::parse($packSim->activated_at); $expirationDate = $activatedAt->copy()->addDays($packSim->package->period); + $expirationDateStart = $expirationDate->copy()->startOfDay(); - // Días restantes - $daysRemaining = (int) now()->diffInDays($expirationDate, false); + if ($expirationDateStart->isFuture()) { + $daysRemaining = (int) $now->diffInDays($expirationDateStart, false); + } elseif ($expirationDateStart->isToday()) { + $daysRemaining = 0; + } else { + // Ya expiró, saltar + continue; + } - // Determinar si debe notificar según el periodo del paquete $shouldNotify = $this->shouldNotifyForPackage( $packSim->package->period, $daysRemaining, @@ -86,7 +79,6 @@ public function handle() ); if ($shouldNotify) { - // Obtener cliente activo $clientRelation = $packSim->simCard->activeClient()->first(); $client = $clientRelation; @@ -95,11 +87,9 @@ public function handle() continue; } - // Mostrar información if ($testMode) { $this->displayNotificationDetails($client, $packSim, $daysRemaining); } else { - // notificacion real $this->sendWhatsAppNotification($client, $packSim, $daysRemaining); } @@ -121,40 +111,25 @@ public function handle() return Command::SUCCESS; } - /** - * Determina si se debe notificar según el periodo del paquete y días restantes - */ private function shouldNotifyForPackage($packagePeriod, $daysRemaining, $customDays = null) { - // Si se especificaron días personalizados if ($customDays !== null) { $notificationDays = array_map('intval', explode(',', $customDays)); return in_array($daysRemaining, $notificationDays); } - // Lógica según el periodo del paquete switch ($packagePeriod) { case 30: - // Paquetes de 30 días: notificar a los 7 días return $daysRemaining === 7; - case 15: - // Paquetes de 15 días: notificar a los 3 días return $daysRemaining === 3; - case 7: - // Paquetes de 7 días: notificar a los 2 días return $daysRemaining === 2; - default: - // Para otros periodos, no notificar (o puedes ajustar la lógica) return false; } } - /** - * Envía notificación por WhatsApp (temporal: solo muestra en consola) - */ private function sendWhatsAppNotification($client, $packSim, $daysRemaining) { $this->warn('PRODUCCIÓN: Aquí se enviaría la notificación real por WhatsApp'); @@ -180,18 +155,27 @@ private function displayNotificationDetails($client, $packSim, $daysRemaining) $this->newLine(); } - private function showAllPackages($packages) { $this->info('Mostrando todos los paquetes activos con sus fechas de vencimiento:'); $this->newLine(); $data = []; + $now = Carbon::now()->startOfDay(); foreach ($packages as $packSim) { $activatedAt = Carbon::parse($packSim->activated_at); $expirationDate = $activatedAt->copy()->addDays($packSim->package->period); - $daysRemaining = (int) now()->diffInDays($expirationDate, false); + $expirationDateStart = $expirationDate->copy()->startOfDay(); + + // ✅ CORRECCIÓN: Calcular días restantes correctamente + if ($expirationDateStart->isFuture()) { + $daysRemaining = (int) $now->diffInDays($expirationDateStart, false); + } elseif ($expirationDateStart->isToday()) { + $daysRemaining = 0; + } else { + $daysRemaining = -1; // Expirado + } $clientRelation = $packSim->simCard->activeClient()->first(); $clientName = $clientRelation ? $clientRelation->full_name : 'Sin cliente activo'; @@ -202,7 +186,7 @@ private function showAllPackages($packages) 'SIM (ICCID)' => $packSim->simCard->iccid, 'Activado' => $activatedAt->format('d/m/Y'), 'Vence el' => $expirationDate->format('d/m/Y'), - 'Días restantes' => $daysRemaining, + 'Días restantes' => $daysRemaining >= 0 ? $daysRemaining : 'Expirado', ]; } diff --git a/app/Http/Controllers/Netbien/PackagesController.php b/app/Http/Controllers/Netbien/PackagesController.php index fa57d96..64294fe 100644 --- a/app/Http/Controllers/Netbien/PackagesController.php +++ b/app/Http/Controllers/Netbien/PackagesController.php @@ -91,7 +91,18 @@ public function expiring(Request $request) // Calcular fecha de vencimiento $activatedAt = Carbon::parse($packSim->activated_at); $expirationDate = $activatedAt->copy()->addDays($packSim->package->period); - $daysRemaining = (int) now()->diffInDays($expirationDate, false); + + $now = Carbon::now()->startOfDay(); + $expirationDateStart = $expirationDate->copy()->startOfDay(); + + if ($expirationDateStart->isFuture()) { + $daysRemaining = (int) $now->diffInDays($expirationDateStart, false); + } elseif ($expirationDateStart->isToday()) { + $daysRemaining = 0; + } else { + // Ya expiró, no incluir + return null; + } // Determinar si debe incluirse $shouldInclude = $this->shouldIncludePackage( diff --git a/database/seeders/DevSeeder.php b/database/seeders/DevSeeder.php index 4538194..a16eb14 100644 --- a/database/seeders/DevSeeder.php +++ b/database/seeders/DevSeeder.php @@ -23,8 +23,6 @@ public function run(): void $this->call(UserSeeder::class); $this->call(SettingSeeder::class); - $this->call(ClientSeeder::class); - $this->call(SimCardSeeder::class); - $this->call(PackageSeeder::class); + //$this->call(PackageSeeder::class); } } diff --git a/routes/api.php b/routes/api.php index e41d042..e55450d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -36,7 +36,7 @@ Route::resource('sales', SaleController::class); Route::get('cash-closes', [CashCloseController::class, 'index']); - Route::put('cash-closes/close', [CashCloseController::class, 'CloseCashClose']); + Route::put('cash-closes/close', [CashCloseController::class, 'closeCashClose']); Route::get('cash-closes/report', [CashCloseController::class, 'report']); Route::get('cash-closes/export', [CashCloseController::class, 'exportReport']); }); -- 2.45.2