From 90f3550d64014504a5f0a8f55f8be665a4467350 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Wed, 19 Nov 2025 21:25:03 +0000 Subject: [PATCH 1/4] Develop (#4) Co-authored-by: Juan Felipe Zapata Moreno Co-committed-by: Juan Felipe Zapata Moreno --- .../Commands/CheckExpiringPackages.php | 198 ++++++++++++++++++ .../Netbien/PackagesController.php | 129 ++++++++++++ app/Models/SimCard.php | 18 ++ database/seeders/DevSeeder.php | 4 +- routes/api.php | 3 +- routes/console.php | 6 + 6 files changed, 354 insertions(+), 4 deletions(-) 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..12b6d2b --- /dev/null +++ b/app/Console/Commands/CheckExpiringPackages.php @@ -0,0 +1,198 @@ +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 + ); + } +} diff --git a/app/Http/Controllers/Netbien/PackagesController.php b/app/Http/Controllers/Netbien/PackagesController.php index 8bbd1d0..64294fe 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,130 @@ 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); + + $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; + } + } } 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/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 e1fe90b..e55450d 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']); @@ -35,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']); }); 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')); From 7486ee089aecfaad2a0c1cddf7c44e0f15161d2d Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Wed, 19 Nov 2025 15:51:49 -0600 Subject: [PATCH 2/4] FIX: Exportar Reporte de corte de caja --- app/Exports/CashCloseReportExport.php | 114 ++++++++++++++++++ .../Netbien/CashCloseController.php | 78 ++---------- 2 files changed, 124 insertions(+), 68 deletions(-) create mode 100644 app/Exports/CashCloseReportExport.php diff --git a/app/Exports/CashCloseReportExport.php b/app/Exports/CashCloseReportExport.php new file mode 100644 index 0000000..6c45644 --- /dev/null +++ b/app/Exports/CashCloseReportExport.php @@ -0,0 +1,114 @@ +cashCloses = $cashCloses; + $this->cashCloseIds = $cashCloses->pluck('id')->toArray(); + } + + public function collection() + { + $data = collect(); + + // RESUMEN FINANCIERO + $totalIncome = $this->cashCloses->sum('income'); + $totalExit = $this->cashCloses->sum('exit'); + $totalCash = $this->cashCloses->sum('income_cash'); + $totalCard = $this->cashCloses->sum('income_card'); + $totalTransfer = $this->cashCloses->sum('income_transfer'); + $balanceFinal = $this->cashCloses->sum('initial_balance') + $totalIncome - $totalExit; + + $data->push(['RESUMEN FINANCIERO', '', '', '', '', '']); + $data->push(['Periodo Inicio', $this->cashCloses->last()?->opened_at, '', '', '', '']); + $data->push(['Periodo Fin', $this->cashCloses->first()?->closed_at, '', '', '', '']); + $data->push(['Total Ventas', $totalIncome, '', '', '', '']); + $data->push(['Efectivo', $totalCash, '', '', '', '']); + $data->push(['Tarjeta', $totalCard, '', '', '', '']); + $data->push(['Transferencia', $totalTransfer, '', '', '', '']); + $data->push(['Egresos', $totalExit, '', '', '', '']); + $data->push(['Balance Final', $balanceFinal, '', '', '', '']); + $data->push(['', '', '', '', '', '']); // Espacios + + // ESTADÍSTICAS POR PAQUETE + $packageStats = DB::table('sale_items') + ->join('sales', 'sale_items.sale_id', '=', 'sales.id') + ->join('packages', 'sale_items.package_id', '=', 'packages.id') + ->whereIn('sales.cash_close_id', $this->cashCloseIds) + ->select( + 'packages.name as paquete', + DB::raw('COUNT(*) as total_vendidos'), + DB::raw('SUM(packages.price) as total_ingresos') + ) + ->groupBy('packages.id', 'packages.name') + ->get(); + + $data->push(['VENTAS POR PAQUETE', '', '', '', '', '']); + $data->push(['Paquete', 'Total Vendidos', 'Total Ingresos', '', '', '']); + foreach ($packageStats as $stat) { + $data->push([$stat->paquete, $stat->total_vendidos, $stat->total_ingresos, '', '', '']); + } + $data->push(['', '', '', '', '', '']); // Espacios + + // VENTAS DETALLADAS + $detailedSales = SaleItem::whereHas('sale', function ($query) { + $query->whereIn('cash_close_id', $this->cashCloseIds); + }) + ->with([ + 'sale.client:id,name,paternal,maternal', + 'sale:id,client_id,payment_method', + 'simCard:id,iccid,msisdn', + 'package:id,name,price' + ]) + ->orderBy('id', 'asc') + ->get(); + + $data->push(['VENTAS DETALLADAS', '', '', '', '', '']); + $data->push(['Nombre Comprador', 'ID SIM', 'Número Asignado', 'Paquete', 'Costo', 'Medio de Pago']); + + foreach ($detailedSales as $item) { + $data->push([ + $item->sale->client->full_name, + $item->simCard->iccid, + $item->simCard->msisdn, + $item->package->name, + $item->package->price, + ucfirst($item->sale->payment_method) + ]); + } + + return $data; + } + + public function headings(): array + { + return []; + } + + public function title(): string + { + return 'Reporte Corte de Caja'; + } + + public function styles(Worksheet $sheet) + { + return [ + 1 => ['font' => ['bold' => true, 'size' => 14]], + 11 => ['font' => ['bold' => true, 'size' => 14]], + ]; + } +} diff --git a/app/Http/Controllers/Netbien/CashCloseController.php b/app/Http/Controllers/Netbien/CashCloseController.php index 34fa184..774e0e1 100644 --- a/app/Http/Controllers/Netbien/CashCloseController.php +++ b/app/Http/Controllers/Netbien/CashCloseController.php @@ -9,7 +9,8 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Notsoweb\ApiResponse\Enums\ApiResponse; -use phpseclib3\Crypt\RC2; +use App\Exports\CashCloseReportExport; +use Maatwebsite\Excel\Facades\Excel; /** * @@ -210,11 +211,11 @@ public function exportReport(Request $request) $query = CashClose::with('user:id,name')->withCount('sales'); if ($request->has('start_date') && $request->has('end_date')) { - $query->whereBetween('close_at', [$request->start_date, $request->end_date]); + $query->whereBetween('closed_at', [$request->start_date, $request->end_date]); } elseif ($request->has('start_date')) { - $query->whereDate('close_at', '>=', $request->start_date); + $query->whereDate('closed_at', '>=', $request->start_date); } elseif ($request->has('end_date')) { - $query->whereDate('close_at', '<=', $request->end_date); + $query->whereDate('closed_at', '<=', $request->end_date); } else { $query->closed()->orderBy('id', 'desc')->limit(1); } @@ -227,70 +228,11 @@ public function exportReport(Request $request) ]); } - $cashCloseIds = $cashCloses->pluck('id')->toArray(); + $filename = 'reporte_corte_caja_' . date('Y-m-d_His') . '.xlsx'; - // Obtener ventas detalladas - $detailedSales = SaleItem::whereHas('sale', function ($query) use ($cashCloseIds) { - $query->whereIn('cash_close_id', $cashCloseIds); - }) - ->with([ - 'sale.client:id,name,paternal,maternal', - 'sale:id,client_id,payment_method', - 'simCard:id,iccid,msisdn', - 'package:id,name,price' - ]) - ->orderBy('id', 'asc') - ->get(); - - // Calcular totales - $totalIncome = $cashCloses->sum('income'); - $totalExit = $cashCloses->sum('exit'); - $totalCash = $cashCloses->sum('income_cash'); - $totalCard = $cashCloses->sum('income_card'); - $totalTransfer = $cashCloses->sum('income_transfer'); - - // Crear el CSV - $filename = 'reporte_corte_caja_' . date('Y-m-d_His') . '.csv'; - - $headers = [ - 'Content-Type' => 'text/csv; charset=UTF-8', - 'Content-Disposition' => 'attachment; filename="' . $filename . '"', - ]; - - $callback = function () use ($cashCloses, $detailedSales, $totalIncome, $totalExit, $totalCash, $totalCard, $totalTransfer) { - $file = fopen('php://output', 'w'); - - fprintf($file, chr(0xEF) . chr(0xBB) . chr(0xBF)); - - // RESUMEN FINANCIERO - fputcsv($file, ['RESUMEN FINANCIERO', '']); - fputcsv($file, ['Periodo Inicio', $cashCloses->last()?->opened_at]); - fputcsv($file, ['Periodo Fin', $cashCloses->first()?->closed_at]); - fputcsv($file, ['Total Ventas', number_format($totalIncome, 2)]); - fputcsv($file, ['Efectivo', number_format($totalCash, 2)]); - fputcsv($file, ['Tarjeta', number_format($totalCard, 2)]); - fputcsv($file, ['Transferencia', number_format($totalTransfer, 2)]); - fputcsv($file, ['Egresos', number_format($totalExit, 2)]); - fputcsv($file, []); - - // VENTAS DETALLADAS - fputcsv($file, ['VENTAS DETALLADAS']); - fputcsv($file, ['Nombre Comprador', 'ID SIM', 'Número Asignado', 'Paquete', 'Costo', 'Medio de Pago']); - - foreach ($detailedSales as $item) { - fputcsv($file, [ - $item->sale->client->full_name, - "'" . $item->simCard->iccid . "'", - "'" . $item->simCard->msisdn . "'", - $item->package->name, - number_format($item->package->price, 2), - ucfirst($item->sale->payment_method) - ]); - } - - fclose($file); - }; - - return response()->stream($callback, 200, $headers); + return Excel::download( + new CashCloseReportExport($cashCloses), + $filename + ); } } From f310bcac512d26b2fc7f4a3187ac9364ce9e9872 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Wed, 19 Nov 2025 16:21:26 -0600 Subject: [PATCH 3/4] FIX: Validaciones sim y paquetes --- app/Http/Controllers/Netbien/SimCardController.php | 10 ++++++---- app/Http/Requests/Netbien/PackagesStoreRequest.php | 6 +++--- app/Http/Requests/Netbien/SimCardStoreRequest.php | 4 ++-- app/Http/Requests/Netbien/SimCardUpdateRequest.php | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/Netbien/SimCardController.php b/app/Http/Controllers/Netbien/SimCardController.php index 7f0d58f..50db6cd 100644 --- a/app/Http/Controllers/Netbien/SimCardController.php +++ b/app/Http/Controllers/Netbien/SimCardController.php @@ -250,10 +250,12 @@ private function getOrCreatePackage(string $type, float $price): Packages return $this->packageCache[$cacheKey]; } - $package = Packages::firstOrCreate( - ['name' => $type, 'price' => $price], - ['period' => 0, 'data_limit' => 0] - ); + $package = Packages::create([ + 'name' => $type, + 'price' => (float) $price, + 'period' => 0, + 'data_limit' => 0, + ]); if ($package->wasRecentlyCreated) { $this->stats['packages_created']++; diff --git a/app/Http/Requests/Netbien/PackagesStoreRequest.php b/app/Http/Requests/Netbien/PackagesStoreRequest.php index fc8ddad..2bb1757 100644 --- a/app/Http/Requests/Netbien/PackagesStoreRequest.php +++ b/app/Http/Requests/Netbien/PackagesStoreRequest.php @@ -26,9 +26,9 @@ public function rules(): array { return [ 'name' => ['required', 'string', 'max:80'], - 'price' => ['required', 'integer'], - 'period' => ['required', 'integer'], - 'data_limit' => ['required', 'integer'], + 'price' => ['required', 'numeric'], + 'period' => ['required', 'numeric'], + 'data_limit' => ['required', 'numeric'], ]; } diff --git a/app/Http/Requests/Netbien/SimCardStoreRequest.php b/app/Http/Requests/Netbien/SimCardStoreRequest.php index d5bb6cc..ba1fbed 100644 --- a/app/Http/Requests/Netbien/SimCardStoreRequest.php +++ b/app/Http/Requests/Netbien/SimCardStoreRequest.php @@ -28,7 +28,7 @@ public function rules(): array { return [ 'iccid' => ['required', 'string', 'max:25', 'unique:sim_cards,iccid'], - 'msisdn' => ['required', 'string', 'max:10', 'unique:sim_cards,msisdn'], + 'msisdn' => ['required', 'string', 'max:15', 'unique:sim_cards,msisdn'], 'package_id' => ['nullable', 'integer', 'exists:packages,id'], ]; } @@ -43,7 +43,7 @@ public function messages() : array 'msisdn.required' => 'El campo MSISDN es obligatorio.', 'msisdn.string' => 'El campo MSISDN debe ser una cadena de texto.', - 'msisdn.max' => 'El campo MSISDN no debe exceder los 10 caracteres.', + 'msisdn.max' => 'El campo MSISDN no debe exceder los 15 caracteres.', 'msisdn.unique' => 'El MSISDN ya está en uso.', 'package_id.integer' => 'El paquete debe ser un número entero.', diff --git a/app/Http/Requests/Netbien/SimCardUpdateRequest.php b/app/Http/Requests/Netbien/SimCardUpdateRequest.php index 384b0aa..c865b9e 100644 --- a/app/Http/Requests/Netbien/SimCardUpdateRequest.php +++ b/app/Http/Requests/Netbien/SimCardUpdateRequest.php @@ -27,7 +27,7 @@ public function authorize(): bool public function rules(): array { return [ - 'msisdn' => ['required', 'string', 'max:10', 'unique:sim_cards,msisdn'], + 'msisdn' => ['required', 'string', 'max:15', 'unique:sim_cards,msisdn'], ]; } From 72f2e4f24dadd9e8123fc452d801fee0231b7188 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Wed, 19 Nov 2025 17:05:22 -0600 Subject: [PATCH 4/4] FIX: Permitir RFC nulo en solicitudes de cliente --- .../Controllers/Netbien/SimCardController.php | 148 +++++++++--------- .../Requests/Netbien/ClientStoreRequest.php | 2 +- .../Requests/Netbien/ClientUpdateRequest.php | 2 +- 3 files changed, 77 insertions(+), 75 deletions(-) diff --git a/app/Http/Controllers/Netbien/SimCardController.php b/app/Http/Controllers/Netbien/SimCardController.php index 50db6cd..6cc528b 100644 --- a/app/Http/Controllers/Netbien/SimCardController.php +++ b/app/Http/Controllers/Netbien/SimCardController.php @@ -126,69 +126,87 @@ public function import(Request $request) $sheet = $spreadsheet->getActiveSheet(); $rows = $sheet->toArray(); - foreach ($rows as $index => $row) { - if ($index === 0) continue; + DB::beginTransaction(); - $this->processRow([ - 'iccid' => $row[4] ?? null, - 'msisdn' => $row[5] ?? null, - 'estado_de_la_sim' => $row[9] ?? null, - 'usuario' => $row[8] ?? null, + try { + foreach ($rows as $index => $row) { + if ($index === 0) continue; + + try { + $this->processRow([ + 'iccid' => $row[4] ?? null, + 'msisdn' => $row[5] ?? null, + 'estado_de_la_sim' => $row[9] ?? null, + 'usuario' => $row[8] ?? null, + ], $index + 1); + } catch (\Exception $e) { + // Capturar información detallada del error antes de revertir + DB::rollBack(); + + return ApiResponse::BAD_REQUEST->response([ + 'success' => false, + 'message' => 'Error en la importación', + 'error' => $e->getMessage(), + 'fila' => $index + 1, + 'datos_fila' => [ + 'iccid' => $row[4] ?? null, + 'msisdn' => $row[5] ?? null, + 'estado_de_la_sim' => $row[9] ?? null, + 'usuario' => $row[8] ?? null, + ], + 'stats' => $this->stats, + ], 500); + } + } + + DB::commit(); + + return ApiResponse::OK->response([ + 'success' => true, + 'message' => 'Importación completada', + 'stats' => $this->stats, + 'packages_created' => array_values(array_map(fn($p) => [ + 'name' => $p->name, + 'price' => $p->price + ], $this->packageCache)), ]); + } catch (\Exception $e) { + DB::rollBack(); + throw $e; } - - return ApiResponse::OK->response([ - 'success' => true, - 'message' => 'Importación completada', - 'stats' => $this->stats, - 'packages_created' => array_values(array_map(fn($p) => [ - 'name' => $p->name, - 'price' => $p->price - ], $this->packageCache)), - ]); } catch (\Exception $e) { return ApiResponse::BAD_REQUEST->response([ 'success' => false, 'message' => 'Error en la importación', 'error' => $e->getMessage(), - 'line' => $e->getLine(), ], 500); } } - private function processRow(array $row) + private function processRow(array $row, int $rowNumber = 0) { // Validar campos requeridos if (empty($row['iccid']) || empty($row['msisdn'])) { return; } - try { - DB::transaction(function () use ($row) { - // Buscar o crear la SIM - $sim = SimCard::where('iccid', $row['iccid'])->first(); + // Buscar o crear la SIM + $sim = SimCard::where('iccid', $row['iccid'])->first(); - if (!$sim) { - // No existe, crearla - $sim = SimCard::create([ - 'iccid' => $row['iccid'], - 'msisdn' => $row['msisdn'], - 'status' => SimCardStatus::AVAILABLE, - ]); + if (!$sim) { + // No existe, crearla + $sim = SimCard::create([ + 'iccid' => $row['iccid'], + 'msisdn' => $row['msisdn'], + 'status' => SimCardStatus::AVAILABLE, + ]); - $this->stats['created']++; - } - - $this->processPackageFromText($sim, $row); - // Asignar cliente - $this->assignToClient($sim, $row); - }); - } catch (\Exception $e) { - $this->stats['errors'][] = [ - 'iccid' => $row['iccid'] ?? 'N/A', - 'error' => $e->getMessage(), - ]; + $this->stats['created']++; } + + $this->processPackageFromText($sim, $row); + // Asignar cliente + $this->assignToClient($sim, $row); } private function processPackageFromText(SimCard $sim, array $row) @@ -283,22 +301,14 @@ private function assignToClient(SimCard $sim, array $row) if (!$client) { $nameParts = $this->splitFullName($usuario); - try { - $client = Client::create([ - 'full_name' => $usuario, - 'name' => $nameParts['name'], - 'paternal' => $nameParts['paternal'], - 'maternal' => $nameParts['maternal'], - ]); + $client = Client::create([ + 'full_name' => $usuario, + 'name' => $nameParts['name'], + 'paternal' => $nameParts['paternal'], + 'maternal' => $nameParts['maternal'], + ]); - $this->stats['clients_created']++; - } catch (\Exception $e) { - $this->stats['errors'][] = [ - 'usuario' => $usuario, - 'error' => 'Error al crear cliente: ' . $e->getMessage() - ]; - return; - } + $this->stats['clients_created']++; } $existingRelation = ClientSim::where('client_id', $client->id) @@ -310,24 +320,16 @@ private function assignToClient(SimCard $sim, array $row) return; } - try { - ClientSim::create([ - 'client_id' => $client->id, - 'sim_card_id' => $sim->id, - 'assigned_at' => now(), - 'is_active' => true, - ]); + ClientSim::create([ + 'client_id' => $client->id, + 'sim_card_id' => $sim->id, + 'assigned_at' => now(), + 'is_active' => true, + ]); - $sim->update(['status' => SimCardStatus::ASSIGNED]); + $sim->update(['status' => SimCardStatus::ASSIGNED]); - $this->stats['assigned']++; - } catch (\Exception $e) { - $this->stats['errors'][] = [ - 'iccid' => $sim->iccid, - 'usuario' => $usuario, - 'error' => 'Error al asignar cliente: ' . $e->getMessage() - ]; - } + $this->stats['assigned']++; } private function splitFullName(string $fullName): array diff --git a/app/Http/Requests/Netbien/ClientStoreRequest.php b/app/Http/Requests/Netbien/ClientStoreRequest.php index 2bfaee5..7c4f86a 100644 --- a/app/Http/Requests/Netbien/ClientStoreRequest.php +++ b/app/Http/Requests/Netbien/ClientStoreRequest.php @@ -32,7 +32,7 @@ public function rules(): array 'maternal' => ['required', 'string'], 'email' => ['nullable', 'email'], 'phone' => ['nullable', 'string', 'max:10'], - 'rfc' => ['required', 'string', 'max:13'], + 'rfc' => ['nullable', 'string', 'max:13'], ]; } diff --git a/app/Http/Requests/Netbien/ClientUpdateRequest.php b/app/Http/Requests/Netbien/ClientUpdateRequest.php index f81c367..7de9d77 100644 --- a/app/Http/Requests/Netbien/ClientUpdateRequest.php +++ b/app/Http/Requests/Netbien/ClientUpdateRequest.php @@ -32,7 +32,7 @@ public function rules(): array 'maternal' => ['sometimes', 'string', 'max:100'], 'email' => ['sometimes', 'email'], 'phone' => ['nullable', 'string', 'max:20'], - 'rfc' => ['sometimes', 'string', 'max:13'], + 'rfc' => ['nullable', 'string', 'max:13'], ]; }