Develop (#4)
Co-authored-by: Juan Felipe Zapata Moreno <juan.zapata@golsystems.com.mx> Co-committed-by: Juan Felipe Zapata Moreno <juan.zapata@golsystems.com.mx>
This commit is contained in:
parent
088ed79a0b
commit
90f3550d64
198
app/Console/Commands/CheckExpiringPackages.php
Normal file
198
app/Console/Commands/CheckExpiringPackages.php
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\PackSim;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CheckExpiringPackages extends Command
|
||||||
|
{
|
||||||
|
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}';
|
||||||
|
|
||||||
|
protected $description = 'Verificar paquetes por vencer y enviar notificaciones a los usuarios';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Verificando paquetes por vencer...');
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
$customDays = $this->option('days');
|
||||||
|
$testMode = $this->option('test');
|
||||||
|
|
||||||
|
if ($testMode) {
|
||||||
|
$this->warn('MODO TEST');
|
||||||
|
$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();
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = Carbon::now()->startOfDay();
|
||||||
|
|
||||||
|
foreach ($activePackages as $packSim) {
|
||||||
|
$packagesChecked++;
|
||||||
|
|
||||||
|
$activatedAt = Carbon::parse($packSim->activated_at);
|
||||||
|
$expirationDate = $activatedAt->copy()->addDays($packSim->package->period);
|
||||||
|
$expirationDateStart = $expirationDate->copy()->startOfDay();
|
||||||
|
|
||||||
|
if ($expirationDateStart->isFuture()) {
|
||||||
|
$daysRemaining = (int) $now->diffInDays($expirationDateStart, false);
|
||||||
|
} elseif ($expirationDateStart->isToday()) {
|
||||||
|
$daysRemaining = 0;
|
||||||
|
} else {
|
||||||
|
// Ya expiró, saltar
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shouldNotify = $this->shouldNotifyForPackage(
|
||||||
|
$packSim->package->period,
|
||||||
|
$daysRemaining,
|
||||||
|
$customDays
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($shouldNotify) {
|
||||||
|
$clientRelation = $packSim->simCard->activeClient()->first();
|
||||||
|
$client = $clientRelation;
|
||||||
|
|
||||||
|
if (!$client) {
|
||||||
|
$this->warn("SIM {$packSim->simCard->iccid} sin cliente activo");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($testMode) {
|
||||||
|
$this->displayNotificationDetails($client, $packSim, $daysRemaining);
|
||||||
|
} else {
|
||||||
|
$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 shouldNotifyForPackage($packagePeriod, $daysRemaining, $customDays = null)
|
||||||
|
{
|
||||||
|
if ($customDays !== null) {
|
||||||
|
$notificationDays = array_map('intval', explode(',', $customDays));
|
||||||
|
return in_array($daysRemaining, $notificationDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($packagePeriod) {
|
||||||
|
case 30:
|
||||||
|
return $daysRemaining === 7;
|
||||||
|
case 15:
|
||||||
|
return $daysRemaining === 3;
|
||||||
|
case 7:
|
||||||
|
return $daysRemaining === 2;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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} ({$packSim->package->period} días)");
|
||||||
|
$this->line(" SIM (ICCID): {$packSim->simCard->iccid}");
|
||||||
|
$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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
$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';
|
||||||
|
|
||||||
|
$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 >= 0 ? $daysRemaining : 'Expirado',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->table(
|
||||||
|
['Cliente', 'Paquete', 'SIM (ICCID)', 'Activado', 'Vence el', 'Días restantes'],
|
||||||
|
$data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,9 @@
|
|||||||
use App\Http\Requests\Netbien\PackagesStoreRequest;
|
use App\Http\Requests\Netbien\PackagesStoreRequest;
|
||||||
use App\Http\Requests\Netbien\PackagesUpdateRequest;
|
use App\Http\Requests\Netbien\PackagesUpdateRequest;
|
||||||
use App\Models\Packages;
|
use App\Models\Packages;
|
||||||
|
use App\Models\PackSim;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,4 +59,130 @@ public function destroy(Packages $package)
|
|||||||
|
|
||||||
return ApiResponse::NO_CONTENT->response();
|
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);
|
||||||
|
|
||||||
|
$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(
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,4 +57,22 @@ public function activePackage()
|
|||||||
->withTimestamps()
|
->withTimestamps()
|
||||||
->limit(1);
|
->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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,6 @@ public function run(): void
|
|||||||
$this->call(UserSeeder::class);
|
$this->call(UserSeeder::class);
|
||||||
$this->call(SettingSeeder::class);
|
$this->call(SettingSeeder::class);
|
||||||
|
|
||||||
$this->call(ClientSeeder::class);
|
//$this->call(PackageSeeder::class);
|
||||||
$this->call(SimCardSeeder::class);
|
|
||||||
$this->call(PackageSeeder::class);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
Route::post('import', [SimCardController::class, 'import']);
|
Route::post('import', [SimCardController::class, 'import']);
|
||||||
Route::resource('sim-cards', SimCardController::class);
|
Route::resource('sim-cards', SimCardController::class);
|
||||||
|
|
||||||
|
Route::get('packages/expiring', [PackagesController::class, 'expiring']);
|
||||||
Route::resource('packages', PackagesController::class);
|
Route::resource('packages', PackagesController::class);
|
||||||
|
|
||||||
Route::get('clients/search', [ClientController::class, 'search']);
|
Route::get('clients/search', [ClientController::class, 'search']);
|
||||||
@ -35,7 +36,7 @@
|
|||||||
Route::resource('sales', SaleController::class);
|
Route::resource('sales', SaleController::class);
|
||||||
|
|
||||||
Route::get('cash-closes', [CashCloseController::class, 'index']);
|
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/report', [CashCloseController::class, 'report']);
|
||||||
Route::get('cash-closes/export', [CashCloseController::class, 'exportReport']);
|
Route::get('cash-closes/export', [CashCloseController::class, 'exportReport']);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,3 +8,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
Schedule::call(new DeleteResetPasswords)->hourly();
|
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'));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user