From af8749abcd2354bbe4c2139b63d23532009e311c Mon Sep 17 00:00:00 2001 From: "juan.zapata" Date: Mon, 10 Nov 2025 22:45:59 +0000 Subject: [PATCH] WIP (#1) Co-authored-by: Juan Felipe Zapata Moreno Reviewed-on: https://gitea.golsystems.mx/juan.zapata/NETBien.backend/pulls/1 --- Docker/nginx/nginx.conf | 10 +- app/Enums/SimCardStatus.php | 28 ++ .../Netbien/CashCloseController.php | 296 ++++++++++++++++++ .../Controllers/Netbien/ClientController.php | 91 ++++++ .../Netbien/PackagesController.php | 59 ++++ .../Controllers/Netbien/SaleController.php | 191 +++++++++++ .../Controllers/Netbien/SimCardController.php | 96 ++++++ .../Requests/Netbien/ClientStoreRequest.php | 48 +++ .../Requests/Netbien/ClientUpdateRequest.php | 48 +++ .../Requests/Netbien/PackagesStoreRequest.php | 51 +++ .../Netbien/PackagesUpdateRequest.php | 53 ++++ .../Requests/Netbien/SaleStoreRequest.php | 58 ++++ .../Requests/Netbien/SaleUpdateRequest.php | 41 +++ .../Requests/Netbien/SimCardStoreRequest.php | 53 ++++ .../Requests/Netbien/SimCardUpdateRequest.php | 42 +++ app/Models/CashClose.php | 62 ++++ app/Models/Client.php | 51 +++ app/Models/ClientSim.php | 36 +++ app/Models/PackSim.php | 43 +++ app/Models/Packages.php | 62 ++++ app/Models/Sale.php | 40 +++ app/Models/SaleItem.php | 38 +++ app/Models/SimCard.php | 60 ++++ app/Services/CashCloseService.php | 33 ++ ...25_11_04_132401_create_sim_cards_table.php | 31 ++ ...025_11_04_132448_create_packages_table.php | 31 ++ ...25_11_04_132537_create_pack_sims_table.php | 29 ++ ..._add_history_fields_to_pack_sims_table.php | 30 ++ ...2025_11_04_215818_create_clients_table.php | 32 ++ ..._11_04_215846_create_client_sims_table.php | 32 ++ .../2025_11_04_221603_create_sales_table.php | 31 ++ ...5_11_04_221707_create_sale_items_table.php | 30 ++ ..._11_05_114844_create_cash_closes_table.php | 33 ++ ...14939_add_cash_close_id_to_sales_table.php | 29 ++ ..._11_05_151608_add_rfc_to_clients_table.php | 28 ++ ...fy_clients_table_rfc_email_constraints.php | 24 ++ ...831_modify_cash_closes_table_structure.php | 55 ++++ database/seeders/ClientSeeder.php | 61 ++++ database/seeders/DevSeeder.php | 8 +- database/seeders/PackageSeeder.php | 25 ++ database/seeders/PackagesSeeder.php | 42 +++ database/seeders/SimCardSeeder.php | 45 +++ docker-compose.yml | 56 ++-- dockerfile | 8 +- entrypoint-dev.sh | 10 +- entrypoint.sh | 105 ------- routes/api.php | 25 +- 47 files changed, 2233 insertions(+), 157 deletions(-) create mode 100644 app/Enums/SimCardStatus.php create mode 100644 app/Http/Controllers/Netbien/CashCloseController.php create mode 100644 app/Http/Controllers/Netbien/ClientController.php create mode 100644 app/Http/Controllers/Netbien/PackagesController.php create mode 100644 app/Http/Controllers/Netbien/SaleController.php create mode 100644 app/Http/Controllers/Netbien/SimCardController.php create mode 100644 app/Http/Requests/Netbien/ClientStoreRequest.php create mode 100644 app/Http/Requests/Netbien/ClientUpdateRequest.php create mode 100644 app/Http/Requests/Netbien/PackagesStoreRequest.php create mode 100644 app/Http/Requests/Netbien/PackagesUpdateRequest.php create mode 100644 app/Http/Requests/Netbien/SaleStoreRequest.php create mode 100644 app/Http/Requests/Netbien/SaleUpdateRequest.php create mode 100644 app/Http/Requests/Netbien/SimCardStoreRequest.php create mode 100644 app/Http/Requests/Netbien/SimCardUpdateRequest.php create mode 100644 app/Models/CashClose.php create mode 100644 app/Models/Client.php create mode 100644 app/Models/ClientSim.php create mode 100644 app/Models/PackSim.php create mode 100644 app/Models/Packages.php create mode 100644 app/Models/Sale.php create mode 100644 app/Models/SaleItem.php create mode 100644 app/Models/SimCard.php create mode 100644 app/Services/CashCloseService.php create mode 100644 database/migrations/2025_11_04_132401_create_sim_cards_table.php create mode 100644 database/migrations/2025_11_04_132448_create_packages_table.php create mode 100644 database/migrations/2025_11_04_132537_create_pack_sims_table.php create mode 100644 database/migrations/2025_11_04_204413_add_history_fields_to_pack_sims_table.php create mode 100644 database/migrations/2025_11_04_215818_create_clients_table.php create mode 100644 database/migrations/2025_11_04_215846_create_client_sims_table.php create mode 100644 database/migrations/2025_11_04_221603_create_sales_table.php create mode 100644 database/migrations/2025_11_04_221707_create_sale_items_table.php create mode 100644 database/migrations/2025_11_05_114844_create_cash_closes_table.php create mode 100644 database/migrations/2025_11_05_114939_add_cash_close_id_to_sales_table.php create mode 100644 database/migrations/2025_11_05_151608_add_rfc_to_clients_table.php create mode 100644 database/migrations/2025_11_07_111354_modify_clients_table_rfc_email_constraints.php create mode 100644 database/migrations/2025_11_07_163831_modify_cash_closes_table_structure.php create mode 100644 database/seeders/ClientSeeder.php create mode 100644 database/seeders/PackageSeeder.php create mode 100644 database/seeders/PackagesSeeder.php create mode 100644 database/seeders/SimCardSeeder.php delete mode 100644 entrypoint.sh diff --git a/Docker/nginx/nginx.conf b/Docker/nginx/nginx.conf index 4ce5974..9d07059 100644 --- a/Docker/nginx/nginx.conf +++ b/Docker/nginx/nginx.conf @@ -1,7 +1,7 @@ server { listen 80; server_name _; - root /var/www/golscontrols/public; + root /var/www/netbien/public; index index.php index.html; # Logging @@ -17,7 +17,7 @@ server { location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass golscontrols:9000; + fastcgi_pass netbien-backend:9000; fastcgi_index index.php; # Timeouts importantes para evitar errores 500 @@ -45,17 +45,17 @@ server { # Handle storage files (Laravel storage link) location /storage { - alias /var/www/golscontrols/storage/app; + alias /var/www/netbien/storage/app; try_files $uri =404; } location /profile { - alias /var/www/golscontrols/storage/app/profile; + alias /var/www/netbien/storage/app/profile; try_files $uri =404; } location /images { - alias /var/www/golscontrols/storage/app/images; + alias /var/www/netbien/storage/app/images; try_files $uri =404; } diff --git a/app/Enums/SimCardStatus.php b/app/Enums/SimCardStatus.php new file mode 100644 index 0000000..59bb2c2 --- /dev/null +++ b/app/Enums/SimCardStatus.php @@ -0,0 +1,28 @@ + 'Disponible', + self::ASSIGNED => 'Asignada', + }; + } +} diff --git a/app/Http/Controllers/Netbien/CashCloseController.php b/app/Http/Controllers/Netbien/CashCloseController.php new file mode 100644 index 0000000..34fa184 --- /dev/null +++ b/app/Http/Controllers/Netbien/CashCloseController.php @@ -0,0 +1,296 @@ +withCount('sales'); + + if ($request->has('status')) { + $query->where('status', $request->status); + } + + if ($request->has('date')) { + $query->whereDate('close_date', $request->date); + } + + $cashCloses = $query->orderBy('id', 'asc') + ->paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'cash_closes' => $cashCloses, + ]); + } + + public function closeCashClose(Request $request) + { + $request->validate([ + 'exit' => 'sometimes|numeric|min:0', + ]); + + $cashClose = CashClose::open()->first(); + + if (!$cashClose) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'No hay un corte de caja abierto para cerrar.', + ]); + } + + $totalSales = Sale::where('cash_close_id', $cashClose->id)->sum('total_amount'); + + $paymentMethods = Sale::where('cash_close_id', $cashClose->id) + ->select('payment_method', DB::raw('SUM(total_amount) as total')) + ->groupBy('payment_method') + ->pluck('total', 'payment_method'); + + $exit = $request->input('exit', 0); + + $cashClose->update([ + 'closed_at' => now(), + 'income' => $totalSales, + 'exit' => $exit, + 'income_cash' => $paymentMethods->get('cash', 0), + 'income_card' => $paymentMethods->get('card', 0), + 'income_transfer' => $paymentMethods->get('transfer', 0), + 'status' => 'closed', + ]); + + $balanceFinal = $cashClose->initial_balance + $totalSales - $exit; + + return ApiResponse::OK->response([ + 'message' => 'Corte de caja cerrado exitosamente.', + 'cash_close' => $cashClose->fresh('user'), + 'resumen' => [ + 'periodo' => [ + 'apertura' => $cashClose->opened_at, + 'cierre' => $cashClose->closed_at, + ], + 'totales' => [ + 'fondo_inicial' => $cashClose->initial_balance, + 'total_ventas' => $totalSales, + 'efectivo' => $paymentMethods->get('cash', 0), + 'tarjeta' => $paymentMethods->get('card', 0), + 'transferencia' => $paymentMethods->get('transfer', 0), + 'egresos' => $exit, + 'balance_final' => $balanceFinal, + ], + ], + ]); + } + + public function report(Request $request) + { + $request->validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + ]); + + $query = CashClose::with('user:id,name')->withCount('sales'); + + if ($request->has('start_date') && $request->has('end_date')) { + $query->whereBetween('close_date', [$request->start_date, $request->end_date]); + } elseif ($request->has('start_date')) { + $query->whereDate('close_date', '>=', $request->start_date); + } elseif ($request->has('end_date')) { + $query->whereDate('close_date', '<=', $request->end_date); + } else { + $query->closed()->orderBy('id', 'desc')->limit(1); + } + + // Información del corte de caja + $cashCloses = $query->orderBy('id', 'desc')->get(); + + if ($cashCloses->isEmpty()) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'No se encontraron cortes de caja en el rango de fechas especificado.', + ]); + } + + $cashCloseIds = $cashCloses->pluck('id')->toArray(); + + // Estadísticas por paquete (Total de Paquetes Vendidos por Tipo) + $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', $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(); + + // Estadísticas por duración (Total de Ventas por Duración) + $durationStats = 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', $cashCloseIds) + ->select( + 'packages.period as duracion_dias', + DB::raw('COUNT(DISTINCT sales.id) as total_ventas') + ) + ->groupBy('packages.period') + ->orderBy('packages.period', 'asc') + ->get(); + + // Reporte detallado de ventas + $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') + ->paginate(config('app.pagination')) + ->through(function ($item) { + return [ + 'nombre_comprador' => $item->sale->client->full_name, + 'id_sim' => $item->simCard->iccid, + 'numero_asignado' => $item->simCard->msisdn, + 'paquete' => $item->package->name, + 'costo' => $item->package->price, + 'medio_pago' => $item->sale->payment_method + ]; + }); + + $totalIncome = $cashCloses->sum('income'); + $totalExit = $cashCloses->sum('exit'); + $totalCash = $cashCloses->sum('income_cash'); + $totalCard = $cashCloses->sum('income_card'); + $totalTransfer = $cashCloses->sum('income_transfer'); + $balanceFinal = $cashCloses->sum('initial_balance') + $totalIncome - $totalExit; + + + return ApiResponse::OK->response([ + 'cash_closes' => $cashCloses, + 'periodo' => [ + 'inicio' => $cashCloses->last()?->opened_at, + 'fin' => $cashCloses->first()?->closed_at, + ], + 'resumen_financiero' => [ + 'total_ventas' => $totalIncome, + 'efectivo' => $totalCash, + 'tarjeta' => $totalCard, + 'transferencia' => $totalTransfer, + 'egresos' => $totalExit, + 'balance_final' => $balanceFinal, + ], + 'ventas_paquete' => $packageStats, + 'ventas_duracion' => $durationStats, + 'ventas_detalladas' => $detailedSales, + ]); + } + + public function exportReport(Request $request) + { + $request->validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + ]); + + $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]); + } elseif ($request->has('start_date')) { + $query->whereDate('close_at', '>=', $request->start_date); + } elseif ($request->has('end_date')) { + $query->whereDate('close_at', '<=', $request->end_date); + } else { + $query->closed()->orderBy('id', 'desc')->limit(1); + } + + $cashCloses = $query->orderBy('id', 'desc')->get(); + + if ($cashCloses->isEmpty()) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'No se encontraron cortes de caja en el rango de fechas especificado.', + ]); + } + + $cashCloseIds = $cashCloses->pluck('id')->toArray(); + + // 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); + } +} diff --git a/app/Http/Controllers/Netbien/ClientController.php b/app/Http/Controllers/Netbien/ClientController.php new file mode 100644 index 0000000..c85d687 --- /dev/null +++ b/app/Http/Controllers/Netbien/ClientController.php @@ -0,0 +1,91 @@ +orderBy('id', 'asc')->paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'data' => $clients, + ]); + } + + public function search(Request $request) + { + $request->validate([ + 'filter' => 'required|string|min:2', + ]); + + $search = $request->input('filter'); + + $clients = Client::with('simCards:id,msisdn') + ->where(function($q) use ($search) { + $q->whereRaw("CONCAT(name, ' ', paternal, ' ', maternal) LIKE ?", ["%{$search}%"]) + ->orWhere('rfc', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%") + ->orWhere('phone', 'like', "%{$search}%"); + }) + ->orderBy('id', 'asc') + ->paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'data' => $clients, + ]); + } + + public function show(Client $client) + { + $client->load('simCards:id,msisdn'); + + return ApiResponse::OK->response([ + 'data' => $client, + ]); + } + + public function store(ClientStoreRequest $request) + { + $client = Client::create($request->validated()); + + return ApiResponse::CREATED->response([ + 'data' => $client, + ]); + } + + public function update(ClientUpdateRequest $request, Client $client) + { + $client->update($request->validated()); + + return ApiResponse::OK->response([ + 'data' => $client, + ]); + } + + public function destroy(Client $client) + { + $hasActiveSims = $client->simCards() + ->wherePivot('is_active', true) + ->exists(); + + if ($hasActiveSims) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'No se puede eliminar el cliente porque tiene SIMs activas', + ]); + } + + $client->delete(); + + return ApiResponse::NO_CONTENT->response(); + } + +} diff --git a/app/Http/Controllers/Netbien/PackagesController.php b/app/Http/Controllers/Netbien/PackagesController.php new file mode 100644 index 0000000..8bbd1d0 --- /dev/null +++ b/app/Http/Controllers/Netbien/PackagesController.php @@ -0,0 +1,59 @@ +paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'data' => $packages, + ]); + } + + public function show(Packages $package) + { + return ApiResponse::OK->response([ + 'data' => $package, + ]); + } + + public function store(PackagesStoreRequest $request) + { + $validated = $request->validated(); + + $package = Packages::create($validated); + + return ApiResponse::CREATED->response([ + 'data' => $package, + ]); + } + + public function update(PackagesUpdateRequest $request, Packages $package) + { + $package->update($request->validated()); + + return ApiResponse::OK->response([ + 'data' => $package, + ]); + } + + public function destroy(Packages $package) + { + $package->delete(); + + return ApiResponse::NO_CONTENT->response(); + } +} diff --git a/app/Http/Controllers/Netbien/SaleController.php b/app/Http/Controllers/Netbien/SaleController.php new file mode 100644 index 0000000..5f3bb4a --- /dev/null +++ b/app/Http/Controllers/Netbien/SaleController.php @@ -0,0 +1,191 @@ +has('date')) { + $query->whereDate('sale_date', $request->date); + } + + // Filtro por cliente + if ($request->has('client_id')) { + $query->where('client_id', $request->client_id); + } + + // Filtro por método de pago + if ($request->has('payment_method')) { + $query->where('payment_method', $request->payment_method); + } + + $sales = $query->orderBy('id', 'asc') + ->paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'Sale' => $sales, + ]); + } + + public function store(SaleStoreRequest $request) + { + try { + DB::beginTransaction(); + + if ($request->has('client_id')) { + $client = Client::findOrFail($request->client_id); + } else { + $client = Client::create($request->client); + } + + $total = 0; + foreach ($request->saleItems as $item) { + $package = Packages::findOrFail($item['package_id']); + $total += $package->price; + } + + $cashClose = CashCloseService::getOrCreateOpenCashClose(); + + $sale = Sale::create([ + 'client_id' => $client->id, + 'cash_close_id' => $cashClose->id, + 'total_amount' => $total, + 'payment_method' => $request->payment_method, + 'sale_date' => now(), + ]); + + foreach ($request->saleItems as $item) { + $sim = SimCard::findOrFail($item['sim_card_id']); + $package = Packages::findOrFail($item['package_id']); + + if ($sim->status !== SimCardStatus::AVAILABLE) { + throw new \Exception("La SIM {$sim->msisdn} no está disponible"); + } + + SaleItem::create([ + 'sale_id' => $sale->id, + 'sim_card_id' => $sim->id, + 'package_id' => $package->id, + ]); + + ClientSim::create([ + 'client_id' => $client->id, + 'sim_card_id' => $sim->id, + 'assigned_at' => now(), + 'is_active' => true, + ]); + + $sim->packages()->attach($package->id, [ + 'activated_at' => now(), + 'is_active' => true, + ]); + + $sim->update(['status' => SimCardStatus::ASSIGNED]); + } + + DB::commit(); + + $sale->load([ + 'client', + 'saleItems.simCard', + 'saleItems.package' + ]); + + return ApiResponse::CREATED->response([ + 'Sale' => $sale, + 'message' => 'Venta registrada exitosamente', + ]); + + } catch (\Exception $e) { + DB::rollBack(); + + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al registrar la venta', + 'error' => $e->getMessage(), + ]); + } + } + + public function update(SaleUpdateRequest $request, Sale $sale) + { + $sale->update($request->only(['payment_method'])); + + return ApiResponse::OK->response([ + 'data' => $sale, + 'message' => 'Venta actualizada exitosamente', + ]); + } + + public function destroy(Sale $sale) + { + try { + DB::beginTransaction(); + + // Obtener todos los items de la venta + $items = $sale->saleItems; + + foreach ($items as $item) { + $sim = $item->simCard; + + // Desactivar el paquete de la SIM + $sim->packages() + ->wherePivot('package_id', $item->package_id) + ->wherePivot('is_active', true) + ->update([ + 'is_active' => false, + 'deactivated_at' => now() + ]); + + //Liberar la SIM del cliente + ClientSim::where('client_id', $sale->client_id) + ->where('sim_card_id', $sim->id) + ->where('is_active', true) + ->update([ + 'is_active' => false, + 'released_at' => now() + ]); + + //Cambiar status de la SIM a disponible + $sim->update(['status' => SimCardStatus::AVAILABLE]); + } + + //Eliminar la venta (cascade) + $sale->delete(); + + DB::commit(); + + return ApiResponse::NO_CONTENT->response(); + + } catch (\Exception $e) { + DB::rollBack(); + + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al cancelar la venta', + 'error' => $e->getMessage(), + ]); + } + } +} diff --git a/app/Http/Controllers/Netbien/SimCardController.php b/app/Http/Controllers/Netbien/SimCardController.php new file mode 100644 index 0000000..a99b778 --- /dev/null +++ b/app/Http/Controllers/Netbien/SimCardController.php @@ -0,0 +1,96 @@ +orderBy('id', 'asc')->paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'data' => $simCards, + ]); + } + + public function show(SimCard $simCard) + { + $simCard->load('packSims.package:id,name'); + + return ApiResponse::OK->response([ + 'data' => $simCard, + ]); + } + + public function store(SimCardStoreRequest $request) + { + try { + DB::beginTransaction(); + + $simCard = SimCard::create($request->validated()); + + if ($request->has('package_id')) { + // Asignar el paquete con fecha de activación + $simCard->packages()->attach($request->package_id, [ + 'activated_at' => now(), + 'is_active' => true, + ]); + } + + DB::commit(); + + $simCard->load('activePackage'); + + return ApiResponse::CREATED->response([ + 'data' => $simCard, + ]); + } catch (\Exception $e) { + DB::rollBack(); + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al crear SIM card', + 'error' => $e->getMessage(), + ]); + } + } + + public function update(SimCardUpdateRequest $request, SimCard $simCard) + { + $simCard->update($request->validated()); + + return ApiResponse::OK->response([ + 'data' => $simCard, + ]); + } + + public function destroy(SimCard $simCard) + { + try { + DB::beginTransaction(); + + $simCard->delete(); + + DB::commit(); + + return ApiResponse::NO_CONTENT->response(); + } catch (\Exception $e) { + DB::rollBack(); + + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al eliminar SIM card', + 'error' => $e->getMessage(), + ]); + } + } + +} diff --git a/app/Http/Requests/Netbien/ClientStoreRequest.php b/app/Http/Requests/Netbien/ClientStoreRequest.php new file mode 100644 index 0000000..2bfaee5 --- /dev/null +++ b/app/Http/Requests/Netbien/ClientStoreRequest.php @@ -0,0 +1,48 @@ + + * + * @version 1.0.0 + */ +class ClientStoreRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string'], + 'paternal' => ['required', 'string'], + 'maternal' => ['required', 'string'], + 'email' => ['nullable', 'email'], + 'phone' => ['nullable', 'string', 'max:10'], + 'rfc' => ['required', 'string', 'max:13'], + ]; + } + + public function messages() : array + { + return [ + 'name.required' => 'El nombre es obligatorio.', + 'paternal.required' => 'El apellido paterno es obligatorio.', + 'maternal.required' => 'El apellido materno es obligatorio.', + 'email.email' => 'El email debe ser válido.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/ClientUpdateRequest.php b/app/Http/Requests/Netbien/ClientUpdateRequest.php new file mode 100644 index 0000000..f81c367 --- /dev/null +++ b/app/Http/Requests/Netbien/ClientUpdateRequest.php @@ -0,0 +1,48 @@ + + * + * @version 1.0.0 + */ +class ClientUpdateRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + 'name' => ['sometimes', 'string', 'max:255'], + 'paternal' => ['sometimes', 'string', 'max:100'], + 'maternal' => ['sometimes', 'string', 'max:100'], + 'email' => ['sometimes', 'email'], + 'phone' => ['nullable', 'string', 'max:20'], + 'rfc' => ['sometimes', 'string', 'max:13'], + ]; + } + + public function messages() : array + { + return [ + 'name.required' => 'El campo Nombre es obligatorio.', + 'email.required' => 'El campo Correo Electrónico es obligatorio.', + 'email.email' => 'El campo Correo Electrónico debe ser una dirección de correo válida.', + 'phone.required' => 'El campo Teléfono es obligatorio.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/PackagesStoreRequest.php b/app/Http/Requests/Netbien/PackagesStoreRequest.php new file mode 100644 index 0000000..fc8ddad --- /dev/null +++ b/app/Http/Requests/Netbien/PackagesStoreRequest.php @@ -0,0 +1,51 @@ + + * + * @version 1.0.0 + */ +class PackagesStoreRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:80'], + 'price' => ['required', 'integer'], + 'period' => ['required', 'integer'], + 'data_limit' => ['required', 'integer'], + ]; + } + + public function messages() : array + { + return [ + 'name.required' => 'El campo Nombre es obligatorio.', + 'name.string' => 'El campo Nombre debe ser una cadena de texto.', + 'name.max' => 'El campo Nombre no debe exceder los 80 caracteres.', + + 'price.required' => 'El campo Precio es obligatorio.', + 'price.min' => 'El campo Precio no debe ser negativo.', + + 'period.required' => 'El campo Periodo es obligatorio.', + + 'data_limit.required' => 'El campo Límite de Datos es obligatorio.', + 'data_limit.min' => 'El campo Límite de Datos no debe ser negativo.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/PackagesUpdateRequest.php b/app/Http/Requests/Netbien/PackagesUpdateRequest.php new file mode 100644 index 0000000..8dc4a18 --- /dev/null +++ b/app/Http/Requests/Netbien/PackagesUpdateRequest.php @@ -0,0 +1,53 @@ + + * + * @version 1.0.0 + */ +class PackagesUpdateRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + 'name' => ['sometimes', 'string', 'max:80'], + 'price' => ['sometimes', 'numeric', 'min:0'], + 'period' => ['sometimes', 'numeric', 'min:1'], + 'data_limit' => ['sometimes', 'integer', 'min:0'], + ]; + } + + public function messages() : array + { + return [ + 'name.required' => 'El campo Nombre es obligatorio.', + 'name.string' => 'El campo Nombre debe ser una cadena de texto.', + 'name.max' => 'El campo Nombre no debe exceder los 80 caracteres.', + + 'price.required' => 'El campo Precio es obligatorio.', + 'price.numeric' => 'El campo Precio debe ser un número.', + 'price.min' => 'El campo Precio no debe ser negativo.', + + 'period.required' => 'El campo Periodo es obligatorio.', + + 'data_limit.required' => 'El campo Límite de Datos es obligatorio.', + 'data_limit.integer' => 'El campo Límite de Datos debe ser un número entero.', + 'data_limit.min' => 'El campo Límite de Datos no debe ser negativo.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/SaleStoreRequest.php b/app/Http/Requests/Netbien/SaleStoreRequest.php new file mode 100644 index 0000000..68fee16 --- /dev/null +++ b/app/Http/Requests/Netbien/SaleStoreRequest.php @@ -0,0 +1,58 @@ + + * + * @version 1.0.0 + */ +class SaleStoreRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + 'client_id' => ['nullable', 'integer', 'exists:clients,id'], + 'client' => ['required_without:client_id', 'array'], + 'client.name' => ['required_with:client', 'string'], + 'client.paternal' => ['required_with:client', 'string'], + 'client.maternal' => ['required_with:client', 'string'], + 'client.email' => ['required_with:client', 'email'], + 'client.phone' => ['nullable', 'string', 'max:10'], + + 'payment_method' => ['required', 'string', 'in:cash,card,transfer'], + + 'saleItems' => ['required', 'array', 'min:1'], + 'saleItems.*.sim_card_id' => ['required', 'integer', 'exists:sim_cards,id'], + 'saleItems.*.package_id' => ['required', 'integer', 'exists:packages,id'], + ]; + } + + public function messages(): array + { + return [ + 'client_id.exists' => 'El cliente seleccionado no existe.', + 'client.required_without' => 'Debe proporcionar un cliente existente o crear uno nuevo.', + 'payment_method.required' => 'El método de pago es obligatorio.', + 'payment_method.in' => 'El método de pago debe ser: efectivo, tarjeta o transferencia.', + 'saleItems.required' => 'Debe agregar al menos un item a la venta.', + 'saleItems.*.sim_card_id.exists' => 'Una de las SIM seleccionadas no existe.', + 'saleItems.*.package_id.exists' => 'Uno de los paquetes seleccionados no existe.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/SaleUpdateRequest.php b/app/Http/Requests/Netbien/SaleUpdateRequest.php new file mode 100644 index 0000000..a1f1ead --- /dev/null +++ b/app/Http/Requests/Netbien/SaleUpdateRequest.php @@ -0,0 +1,41 @@ + + * + * @version 1.0.0 + */ +class SaleUpdateRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * + * IMPORTANTE: Generalmente no se permite modificar los items de una venta + * ya registrada por temas de trazabilidad contable. Solo se permite + * actualizar campos administrativos como método de pago o notas. + */ + public function rules(): array + { + return [ + 'payment_method' => ['sometimes', 'string', 'in:cash,card,transfer'], + ]; + } + + public function messages(): array + { + return [ + 'payment_method.in' => 'El método de pago debe ser: efectivo, tarjeta o transferencia.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/SimCardStoreRequest.php b/app/Http/Requests/Netbien/SimCardStoreRequest.php new file mode 100644 index 0000000..d5bb6cc --- /dev/null +++ b/app/Http/Requests/Netbien/SimCardStoreRequest.php @@ -0,0 +1,53 @@ + + * + * @version 1.0.0 + */ +class SimCardStoreRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + 'iccid' => ['required', 'string', 'max:25', 'unique:sim_cards,iccid'], + 'msisdn' => ['required', 'string', 'max:10', 'unique:sim_cards,msisdn'], + 'package_id' => ['nullable', 'integer', 'exists:packages,id'], + ]; + } + + public function messages() : array + { + return [ + 'iccid.required' => 'El campo ICCID es obligatorio.', + 'iccid.string' => 'El campo ICCID debe ser una cadena de texto.', + 'iccid.max' => 'El campo ICCID no debe exceder los 25 caracteres.', + 'iccid.unique' => 'El ICCID ya está en uso.', + + '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.unique' => 'El MSISDN ya está en uso.', + + 'package_id.integer' => 'El paquete debe ser un número entero.', + 'package_id.exists' => 'El paquete seleccionado no existe.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/SimCardUpdateRequest.php b/app/Http/Requests/Netbien/SimCardUpdateRequest.php new file mode 100644 index 0000000..384b0aa --- /dev/null +++ b/app/Http/Requests/Netbien/SimCardUpdateRequest.php @@ -0,0 +1,42 @@ + + * + * @version 1.0.0 + */ +class SimCardUpdateRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + 'msisdn' => ['required', 'string', 'max:10', 'unique:sim_cards,msisdn'], + ]; + } + + public function messages() : array + { + return [ + 'msisdn.required' => 'El campo MSISDN es obligatorio.', + 'msisdn.max' => 'El campo MSISDN no debe exceder los 10 caracteres.', + 'msisdn.unique' => 'El MSISDN ya está en uso.', + ]; + } +} diff --git a/app/Models/CashClose.php b/app/Models/CashClose.php new file mode 100644 index 0000000..3ca6cef --- /dev/null +++ b/app/Models/CashClose.php @@ -0,0 +1,62 @@ + 'decimal:2', + 'income' => 'decimal:2', + 'exit' => 'decimal:2', + 'income_cash' => 'decimal:2', + 'income_card' => 'decimal:2', + 'income_transfer' => 'decimal:2', + 'opened_at' => 'datetime', + 'closed_at' => 'datetime', + ]; + + /** + * Relación con el usuario que realizó el corte + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Relación con las ventas asociadas a este corte + */ + public function sales() + { + return $this->hasMany(Sale::class); + } + + /** + * Scope para cortes abiertos + */ + public function scopeOpen($query) + { + return $query->where('status', 'open'); + } + + /** + * Scope para cortes cerrados + */ + public function scopeClosed($query) + { + return $query->where('status', 'closed'); + } +} diff --git a/app/Models/Client.php b/app/Models/Client.php new file mode 100644 index 0000000..df3baa0 --- /dev/null +++ b/app/Models/Client.php @@ -0,0 +1,51 @@ + + * + * @version 1.0.0 + */ +class Client extends Model +{ + protected $fillable = [ + 'name', + 'paternal', + 'maternal', + 'email', + 'phone', + 'rfc', + ]; + + public function sales() + { + return $this->hasMany(Sale::class); + } + + public function clientSims() + { + return $this->hasMany(ClientSim::class); + } + + public function simCards() + { + return $this->belongsToMany(SimCard::class, 'client_sims') + ->withPivot('assigned_at', 'released_at', 'is_active') + ->withTimestamps(); + } + + public function fullName(): Attribute + { + return Attribute::make( + get: fn () => $this->name . ' ' . $this->paternal . ' ' . $this->maternal, + ); + } +} diff --git a/app/Models/ClientSim.php b/app/Models/ClientSim.php new file mode 100644 index 0000000..d7db83b --- /dev/null +++ b/app/Models/ClientSim.php @@ -0,0 +1,36 @@ + + * + * @version 1.0.0 + */ +class ClientSim extends Model +{ + protected $fillable = [ + 'client_id', + 'sim_card_id', + 'assigned_at', + 'released_at', + 'is_active', + ]; + + + public function client() + { + return $this->belongsTo(Client::class); + } + + public function simCard() + { + return $this->belongsTo(SimCard::class); + } +} diff --git a/app/Models/PackSim.php b/app/Models/PackSim.php new file mode 100644 index 0000000..89209c5 --- /dev/null +++ b/app/Models/PackSim.php @@ -0,0 +1,43 @@ + + * + * @version 1.0.0 + */ +class PackSim extends Model +{ + protected $fillable = [ + 'package_id', + 'sim_card_id', + 'activated_at', + 'deactivated_at', + 'is_active', + ]; + + protected $casts = [ + 'package_id' => 'integer', + 'sim_card_id' => 'integer', + 'activated_at' => 'datetime', + 'deactivated_at' => 'datetime', + 'is_active' => 'boolean', + ]; + + public function package() + { + return $this->belongsTo(Packages::class, 'package_id'); + } + + public function simCard() + { + return $this->belongsTo(SimCard::class, 'sim_card_id'); + } +} diff --git a/app/Models/Packages.php b/app/Models/Packages.php new file mode 100644 index 0000000..d601253 --- /dev/null +++ b/app/Models/Packages.php @@ -0,0 +1,62 @@ + + * + * @version 1.0.0 + */ +class Packages extends Model +{ + protected $fillable = [ + 'name', + 'price', + 'period', + 'data_limit', + ]; + + protected $casts = [ + 'name' => 'string', + 'price' => 'float', + 'period' => 'integer', + 'data_limit' => 'integer', + ]; + + // Relación con la tabla pivote + public function packSims() + { + return $this->hasMany(PackSim::class, 'package_id'); + } + + // Relación muchos a muchos con SIM cards + public function simCards() + { + return $this->belongsToMany( + SimCard::class, + 'pack_sims', + 'package_id', + 'sim_card_id' + )->withPivot('activated_at', 'deactivated_at', 'is_active') + ->withTimestamps(); + } + + // SIM cards activas con este paquete + public function activeSimCards() + { + return $this->belongsToMany( + SimCard::class, + 'pack_sims', + 'package_id', + 'sim_card_id' + )->wherePivot('is_active', true) + ->withPivot('activated_at', 'deactivated_at', 'is_active') + ->withTimestamps(); + } +} diff --git a/app/Models/Sale.php b/app/Models/Sale.php new file mode 100644 index 0000000..3e83c78 --- /dev/null +++ b/app/Models/Sale.php @@ -0,0 +1,40 @@ + + * + * @version 1.0.0 + */ +class Sale extends Model +{ + protected $fillable = [ + 'client_id', + 'cash_close_id', + 'total_amount', + 'payment_method', + 'sale_date', + ]; + + public function client() + { + return $this->belongsTo(Client::class); + } + + public function saleItems() + { + return $this->hasMany(SaleItem::class); + } + + public function cashClose() + { + return $this->belongsTo(CashClose::class); + } +} diff --git a/app/Models/SaleItem.php b/app/Models/SaleItem.php new file mode 100644 index 0000000..a596c62 --- /dev/null +++ b/app/Models/SaleItem.php @@ -0,0 +1,38 @@ + + * + * @version 1.0.0 + */ +class SaleItem extends Model +{ + protected $fillable = [ + 'sale_id', + 'sim_card_id', + 'package_id', + ]; + + public function sale() + { + return $this->belongsTo(Sale::class); + } + + public function simCard() + { + return $this->belongsTo(SimCard::class); + } + + public function package() + { + return $this->belongsTo(Packages::class); + } +} diff --git a/app/Models/SimCard.php b/app/Models/SimCard.php new file mode 100644 index 0000000..1eacd84 --- /dev/null +++ b/app/Models/SimCard.php @@ -0,0 +1,60 @@ + + * + * @version 1.0.0 + */ +class SimCard extends Model +{ + protected $fillable = [ + 'iccid', + 'msisdn', + 'status', + ]; + + protected $casts = [ + 'status' => SimCardStatus::class, + ]; + + // Relación con la tabla pivote + public function packSims() + { + return $this->hasMany(PackSim::class, 'sim_card_id'); + } + + // Relación muchos a muchos con paquetes + public function packages() + { + return $this->belongsToMany( + Packages::class, + 'pack_sims', + 'sim_card_id', + 'package_id' + )->withPivot('activated_at', 'deactivated_at', 'is_active') + ->withTimestamps(); + } + + // Paquete actualmente activo + public function activePackage() + { + return $this->belongsToMany( + Packages::class, + 'pack_sims', + 'sim_card_id', + 'package_id' + )->wherePivot('is_active', true) + ->withPivot('activated_at', 'deactivated_at', 'is_active') + ->withTimestamps() + ->limit(1); + } +} diff --git a/app/Services/CashCloseService.php b/app/Services/CashCloseService.php new file mode 100644 index 0000000..7a9f5f4 --- /dev/null +++ b/app/Services/CashCloseService.php @@ -0,0 +1,33 @@ +first(); + + if (!$cashClose) { + $cashClose = CashClose::create([ + 'user_id' => Auth::id(), + 'opened_at' => now(), + 'initial_balance' => 0, + 'income' => 0, + 'exit' => 0, + 'income_cash' => 0, + 'income_card' => 0, + 'income_transfer' => 0, + 'status' => 'open', + ]); + } + + return $cashClose; + } +} diff --git a/database/migrations/2025_11_04_132401_create_sim_cards_table.php b/database/migrations/2025_11_04_132401_create_sim_cards_table.php new file mode 100644 index 0000000..cbb29ef --- /dev/null +++ b/database/migrations/2025_11_04_132401_create_sim_cards_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('iccid')->unique(); + $table->string('msisdn')->unique(); + $table->enum('status', SimCardStatus::values())->default(SimCardStatus::AVAILABLE->value); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sim_cards'); + } +}; diff --git a/database/migrations/2025_11_04_132448_create_packages_table.php b/database/migrations/2025_11_04_132448_create_packages_table.php new file mode 100644 index 0000000..41b42f0 --- /dev/null +++ b/database/migrations/2025_11_04_132448_create_packages_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->float('price'); + $table->integer('period'); + $table->float('data_limit'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('packages'); + } +}; diff --git a/database/migrations/2025_11_04_132537_create_pack_sims_table.php b/database/migrations/2025_11_04_132537_create_pack_sims_table.php new file mode 100644 index 0000000..d554d53 --- /dev/null +++ b/database/migrations/2025_11_04_132537_create_pack_sims_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('sim_card_id')->constrained('sim_cards')->onDelete('cascade'); + $table->foreignId('package_id')->constrained('packages')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('pack_sims'); + } +}; diff --git a/database/migrations/2025_11_04_204413_add_history_fields_to_pack_sims_table.php b/database/migrations/2025_11_04_204413_add_history_fields_to_pack_sims_table.php new file mode 100644 index 0000000..e22d5a1 --- /dev/null +++ b/database/migrations/2025_11_04_204413_add_history_fields_to_pack_sims_table.php @@ -0,0 +1,30 @@ +timestamp('activated_at')->nullable()->after('package_id'); + $table->timestamp('deactivated_at')->nullable()->after('activated_at'); + $table->boolean('is_active')->default(true)->after('deactivated_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('pack_sims', function (Blueprint $table) { + $table->dropColumn(['activated_at', 'deactivated_at', 'is_active']); + }); + } +}; diff --git a/database/migrations/2025_11_04_215818_create_clients_table.php b/database/migrations/2025_11_04_215818_create_clients_table.php new file mode 100644 index 0000000..078aa48 --- /dev/null +++ b/database/migrations/2025_11_04_215818_create_clients_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('paternal'); + $table->string('maternal'); + $table->string('email')->unique(); + $table->string('phone')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('clients'); + } +}; diff --git a/database/migrations/2025_11_04_215846_create_client_sims_table.php b/database/migrations/2025_11_04_215846_create_client_sims_table.php new file mode 100644 index 0000000..7237acf --- /dev/null +++ b/database/migrations/2025_11_04_215846_create_client_sims_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('client_id')->constrained('clients')->onDelete('cascade'); + $table->foreignId('sim_card_id')->constrained('sim_cards')->onDelete('cascade'); + $table->date('assigned_at'); + $table->date('released_at')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('client_sims'); + } +}; diff --git a/database/migrations/2025_11_04_221603_create_sales_table.php b/database/migrations/2025_11_04_221603_create_sales_table.php new file mode 100644 index 0000000..acce9ae --- /dev/null +++ b/database/migrations/2025_11_04_221603_create_sales_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('client_id')->constrained('clients')->onDelete('cascade'); + $table->decimal('total_amount', 10, 2); + $table->string('payment_method'); + $table->date('sale_date'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sales'); + } +}; diff --git a/database/migrations/2025_11_04_221707_create_sale_items_table.php b/database/migrations/2025_11_04_221707_create_sale_items_table.php new file mode 100644 index 0000000..ee34278 --- /dev/null +++ b/database/migrations/2025_11_04_221707_create_sale_items_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('sale_id')->constrained('sales')->onDelete('cascade'); + $table->foreignId('sim_card_id')->constrained('sim_cards')->onDelete('cascade'); + $table->foreignId('package_id')->constrained('packages')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sale_items'); + } +}; diff --git a/database/migrations/2025_11_05_114844_create_cash_closes_table.php b/database/migrations/2025_11_05_114844_create_cash_closes_table.php new file mode 100644 index 0000000..b56d019 --- /dev/null +++ b/database/migrations/2025_11_05_114844_create_cash_closes_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null'); + $table->decimal('balance', 10, 2); + $table->decimal('income', 10, 2); + $table->decimal('exit', 10, 2)->default(0); + $table->enum('status', ['open', 'closed'])->default('open')->comment('Estado del corte'); + $table->timestamp('close_date'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cash_closes'); + } +}; diff --git a/database/migrations/2025_11_05_114939_add_cash_close_id_to_sales_table.php b/database/migrations/2025_11_05_114939_add_cash_close_id_to_sales_table.php new file mode 100644 index 0000000..876f9cb --- /dev/null +++ b/database/migrations/2025_11_05_114939_add_cash_close_id_to_sales_table.php @@ -0,0 +1,29 @@ +foreignId('cash_close_id')->nullable()->after('client_id')->constrained('cash_closes')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('sales', function (Blueprint $table) { + $table->dropForeign(['cash_close_id']); + $table->dropColumn('cash_close_id'); + }); + } +}; diff --git a/database/migrations/2025_11_05_151608_add_rfc_to_clients_table.php b/database/migrations/2025_11_05_151608_add_rfc_to_clients_table.php new file mode 100644 index 0000000..b782ff5 --- /dev/null +++ b/database/migrations/2025_11_05_151608_add_rfc_to_clients_table.php @@ -0,0 +1,28 @@ +string('rfc')->nullable()->after('phone'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('clients', function (Blueprint $table) { + $table->dropColumn('rfc'); + }); + } +}; diff --git a/database/migrations/2025_11_07_111354_modify_clients_table_rfc_email_constraints.php b/database/migrations/2025_11_07_111354_modify_clients_table_rfc_email_constraints.php new file mode 100644 index 0000000..d841275 --- /dev/null +++ b/database/migrations/2025_11_07_111354_modify_clients_table_rfc_email_constraints.php @@ -0,0 +1,24 @@ +string('rfc', 13)->unique()->change(); + $table->string('phone')->unique()->change(); + }); + } + + public function down(): void + { + // + } +}; diff --git a/database/migrations/2025_11_07_163831_modify_cash_closes_table_structure.php b/database/migrations/2025_11_07_163831_modify_cash_closes_table_structure.php new file mode 100644 index 0000000..6ea14a6 --- /dev/null +++ b/database/migrations/2025_11_07_163831_modify_cash_closes_table_structure.php @@ -0,0 +1,55 @@ +timestamp('opened_at')->nullable()->after('user_id'); + + $table->decimal('initial_balance', 10, 2)->default(0)->after('user_id'); + + $table->dropColumn('balance'); + + $table->decimal('income_cash', 10, 2)->default(0)->after('income'); + $table->decimal('income_card', 10, 2)->default(0)->after('income_cash'); + $table->decimal('income_transfer', 10, 2)->default(0)->after('income_card'); + }); + + // Renombrar y hacer nullable en una segunda operación + Schema::table('cash_closes', function (Blueprint $table) { + $table->renameColumn('close_date', 'closed_at'); + }); + + // Modificar closed_at para que sea nullable + Schema::table('cash_closes', function (Blueprint $table) { + $table->timestamp('closed_at')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('cash_closes', function (Blueprint $table) { + $table->timestamp('closed_at')->nullable(false)->change(); + }); + + Schema::table('cash_closes', function (Blueprint $table) { + $table->renameColumn('closed_at', 'close_date'); + }); + + Schema::table('cash_closes', function (Blueprint $table) { + $table->dropColumn([ + 'opened_at', + 'initial_balance', + 'income_cash', + 'income_card', + 'income_transfer', + ]); + $table->decimal('balance', 10, 2)->after('user_id'); + }); + } +}; diff --git a/database/seeders/ClientSeeder.php b/database/seeders/ClientSeeder.php new file mode 100644 index 0000000..6fbbe01 --- /dev/null +++ b/database/seeders/ClientSeeder.php @@ -0,0 +1,61 @@ + 'Juan', + 'paternal' => 'Pérez', + 'maternal' => 'Gómez', + 'email' => 'juan.perez@example.com', + 'phone' => '551234567890', + 'rfc' => 'JUAP890123XXX', + ], + [ + 'name' => 'María', + 'paternal' => 'López', + 'maternal' => 'Hernández', + 'email' => 'maria.lopez@example.com', + 'phone' => '551234567891', + 'rfc' => 'MALO910203XXX', + ], + [ + 'name' => 'María', + 'paternal' => 'Hernández', + 'maternal' => 'Cruz', + 'email' => 'maria.hernandez@example.com', + 'phone' => '555-1003', + 'rfc' => 'MAHC910203XXX', + ], + [ + 'name' => 'Carlos', + 'paternal' => 'Sánchez', + 'maternal' => 'Ruiz', + 'email' => 'carlos.sanchez@example.com', + 'phone' => '555-1004', + 'rfc' => 'CASR910203XXX', + ], + [ + 'name' => 'Laura', + 'paternal' => 'Gómez', + 'maternal' => 'Flores', + 'email' => 'laura.gomez@example.com', + 'phone' => '555-1005', + 'rfc' => 'LAGF910203XXX', + ] + ]; + + foreach ($clients as $client) { + Client::create($client); + } + } +} diff --git a/database/seeders/DevSeeder.php b/database/seeders/DevSeeder.php index ffea706..4538194 100644 --- a/database/seeders/DevSeeder.php +++ b/database/seeders/DevSeeder.php @@ -7,9 +7,9 @@ /** * Seeder de desarrollo - * + * * @author Moisés Cortés C. - * + * * @version 1.0.0 */ class DevSeeder extends Seeder @@ -22,5 +22,9 @@ public function run(): void $this->call(RoleSeeder::class); $this->call(UserSeeder::class); $this->call(SettingSeeder::class); + + $this->call(ClientSeeder::class); + $this->call(SimCardSeeder::class); + $this->call(PackageSeeder::class); } } diff --git a/database/seeders/PackageSeeder.php b/database/seeders/PackageSeeder.php new file mode 100644 index 0000000..3eef2ec --- /dev/null +++ b/database/seeders/PackageSeeder.php @@ -0,0 +1,25 @@ + + * + * @version 1.0.0 + */ +class PackageSeeder extends Seeder +{ + /** + * Ejecutar sembrado de base de datos + */ + public function run(): void + { + // + } +} diff --git a/database/seeders/PackagesSeeder.php b/database/seeders/PackagesSeeder.php new file mode 100644 index 0000000..07b8521 --- /dev/null +++ b/database/seeders/PackagesSeeder.php @@ -0,0 +1,42 @@ + 'Paquete 1', + 'price' => 100.00, + 'period' => 15, + 'data_limit' => 5, + ], + [ + 'name' => 'Paquete 2', + 'price' => 150.00, + 'period' => 20, + 'data_limit' => 10, + ], + [ + 'name' => 'Paquete 3', + 'price' => 200.00, + 'period' => 25, + 'data_limit' => 15, + ], + [ + 'name' => 'Paquete Premium', + 'price' => 250.00, + 'period' => 30, + 'data_limit' => 20, + ], + ]; + + foreach ($packages as $package) { + Packages::create($package); + } + } +} diff --git a/database/seeders/SimCardSeeder.php b/database/seeders/SimCardSeeder.php new file mode 100644 index 0000000..26f49e4 --- /dev/null +++ b/database/seeders/SimCardSeeder.php @@ -0,0 +1,45 @@ + + * + * @version 1.0.0 + */ +class SimCardSeeder extends Seeder +{ + /** + * Ejecutar sembrado de base de datos + */ + public function run(): void + { + $availables=[ + ['iccid'=>'8986002212345678901','msisdn'=>'551234567890','status'=>'available'], + ['iccid'=>'8986002212345678902','msisdn'=>'551234567891','status'=>'available'], + ['iccid'=>'8986002212345678903','msisdn'=>'551234567892','status'=>'available'], + ['iccid'=>'8986002212345678904','msisdn'=>'551234567893','status'=>'available'], + ['iccid'=>'8986002212345678905','msisdn'=>'551234567894','status'=>'available'], + ['iccid'=>'8986002212345678906','msisdn'=>'551234567895','status'=>'available'], + ['iccid'=>'8986002212345678907','msisdn'=>'551234567896','status'=>'available'], + ['iccid'=>'8986002212345678908','msisdn'=>'551234567897','status'=>'available'], + ['iccid'=>'8986002212345678909','msisdn'=>'551234567898','status'=>'available'], + ['iccid'=>'8986002212345678910','msisdn'=>'551234567899','status'=>'available'], + ]; + + foreach ($availables as $simcard) { + SimCard::create([ + 'iccid' => $simcard['iccid'], + 'msisdn' => $simcard['msisdn'], + 'status' => $simcard['status'], + ]); + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 3b1c760..ecdad7e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,37 +1,40 @@ services: - repuve-backend: + netbien-backend: build: context: . - dockerfile: dockerfile.dev - working_dir: /var/www/repuve-v1 + dockerfile: dockerfile + working_dir: /var/www/netbien environment: - - DB_HOST=mysql + - DB_HOST=${DB_HOST} - DB_USERNAME=${DB_USERNAME} - DB_PASSWORD=${DB_PASSWORD} - DB_DATABASE=${DB_DATABASE} - DB_PORT=${DB_PORT} volumes: - - ./:/var/www/repuve-v1 - - /var/www/repuve-v1/vendor + - ./:/var/www/netbien + - ./vendor:/var/www/netbien/vendor + - /var/www/netbien/node_modules networks: - - repuve-network + - netbien-network + mem_limit: 512m + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: mysql: condition: service_healthy - redis: - condition: service_healthy nginx: image: nginx:alpine ports: - "${NGINX_PORT}:80" volumes: - - ./public:/var/www/repuve-v1/public + - ./:/var/www/netbien - ./Docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf networks: - - repuve-network + - netbien-network + mem_limit: 512m depends_on: - - repuve-backend + - netbien-backend mysql: image: mysql:8.0 @@ -41,11 +44,12 @@ services: MYSQL_PASSWORD: ${DB_PASSWORD} MYSQL_USER: ${DB_USERNAME} ports: - - ${DB_PORT}:${DB_PORT} + - "${DB_PORT}:3306" volumes: - mysql_data:/var/lib/mysql networks: - - repuve-network + - netbien-network + mem_limit: 512m healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 15s @@ -57,31 +61,17 @@ services: PMA_HOST: mysql PMA_PORT: 3306 ports: - - '${PMA_PORT}:80' + - "${PMA_PORT}:80" depends_on: - - mysql + - mysql networks: - - repuve-network - - redis: - image: redis:alpine - ports: - - "${REDIS_PORT}:6379" - volumes: - - redis_data:/data - networks: - - repuve-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - timeout: 5s - retries: 5 + - netbien-network + mem_limit: 512m volumes: mysql_data: driver: local - redis_data: - driver: local networks: - repuve-network: + netbien-network: driver: bridge diff --git a/dockerfile b/dockerfile index 7cee8a0..714f9f8 100644 --- a/dockerfile +++ b/dockerfile @@ -1,8 +1,8 @@ FROM php:8.3-fpm -RUN mkdir -p /var/www/repuve-v1 +RUN mkdir -p /var/www/netbien -WORKDIR /var/www/repuve-v1 +WORKDIR /var/www/netbien RUN apt-get update && apt-get install -y\ git \ @@ -31,8 +31,8 @@ RUN chmod +x /usr/local/bin/entrypoint-dev.sh RUN mkdir -p storage/app/keys storage/logs bootstrap/cache -RUN chown -R www-data:www-data /var/www/repuve-v1/storage /var/www/repuve-v1/bootstrap/cache -RUN chmod -R 775 /var/www/repuve-v1/storage /var/www/repuve-v1/bootstrap/cache +RUN chown -R www-data:www-data /var/www/netbien/storage /var/www/netbien/bootstrap/cache +RUN chmod -R 775 /var/www/netbien/storage /var/www/netbien/bootstrap/cache EXPOSE 9000 diff --git a/entrypoint-dev.sh b/entrypoint-dev.sh index 932cb96..0729c6b 100644 --- a/entrypoint-dev.sh +++ b/entrypoint-dev.sh @@ -1,15 +1,15 @@ #!/bin/bash set -e -git config --global --add safe.directory /var/www/repuve-v1 +git config --global --add safe.directory /var/www/netbien echo "=== Iniciando entrypoint DESARROLLO ===" # Variables desde Docker environment -DB_HOST=${DB_HOST:-mysql} -DB_USERNAME=${DB_USERNAME:-root} -DB_PASSWORD=${DB_PASSWORD:-} -DB_DATABASE=${DB_DATABASE:-laravel} +DB_HOST=${DB_HOST} +DB_USERNAME=${DB_USERNAME} +DB_PASSWORD=${DB_PASSWORD} +DB_DATABASE=${DB_DATABASE} MAX_RETRIES=30 RETRY_COUNT=0 diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 0bf890d..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/bash -set -e - -echo "=== Iniciando entrypoint ===" - -# Variables desde Docker environment -DB_HOST=${DB_HOST:-mysql} -DB_USERNAME=${DB_USERNAME:-root} -DB_PASSWORD=${DB_PASSWORD:-} -DB_DATABASE=${DB_DATABASE:-laravel} -MAX_RETRIES=30 -RETRY_COUNT=0 - -echo "Configuración de BD: Host=${DB_HOST}, Usuario=${DB_USERNAME}, Base=${DB_DATABASE}" - -# Función para verificar conectividad con MySQL usando PHP -check_mysql() { - php -r " - try { - \$pdo = new PDO('mysql:host=${DB_HOST};port=3306', '${DB_USERNAME}', '${DB_PASSWORD}'); - \$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - exit(0); - } catch (Exception \$e) { - exit(1); - } - " -} - -# Esperar a que MySQL esté disponible -echo "Esperando conexión a MySQL..." -until check_mysql; do - RETRY_COUNT=$((RETRY_COUNT + 1)) - if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then - echo "ERROR: No se pudo conectar a MySQL después de $MAX_RETRIES intentos" - exit 1 - fi - echo "Intento $RETRY_COUNT/$MAX_RETRIES - Esperando a MySQL..." - sleep 2 -done - -echo "✓ MySQL está disponible" - -# Comandos de inicialización -echo "Ejecutando comandos de inicialización..." - -echo "Ejecutando package:discover..." -php artisan package:discover --ansi - -echo "Creando enlaces simbólicos..." -php artisan storage:link --force || true - -echo "Ejecutando configuración de producción..." -composer run env:prod - -echo "Creando directorio de claves Passport..." -mkdir -p storage/app/keys - -echo "Generando claves de Passport..." -php artisan passport:keys --force || true - -# Verificar que las claves se crearon -if [ ! -f "storage/app/keys/oauth-private.key" ] || [ ! -f "storage/app/keys/oauth-public.key" ]; then - echo "ERROR: Las claves de Passport no se generaron correctamente" - echo "Intentando generar manualmente..." - - # Generar claves manualmente usando OpenSSL - openssl genrsa -out storage/app/keys/oauth-private.key 4096 - openssl rsa -in storage/app/keys/oauth-private.key -pubout -out storage/app/keys/oauth-public.key - - echo "✓ Claves generadas manualmente" -fi - -# Establecer permisos correctos para las claves -chmod 600 storage/app/keys/oauth-private.key -chmod 644 storage/app/keys/oauth-public.key -chown www-data:www-data storage/app/keys/oauth-*.key - -echo "✓ Claves de Passport verificadas" - -# Archivo de control para primera ejecución -FIRST_RUN_FLAG="/var/www/holos.backend/.first_run_completed" - -# Solo en la primera ejecución -if [ ! -f "$FIRST_RUN_FLAG" ]; then - echo "=== PRIMERA EJECUCIÓN DETECTADA ===" - - echo "Ejecutando migraciones y seeders..." - if composer run db:prod; then - echo "✓ db:prod completado" - else - echo "ERROR: Falló db:prod" - exit 1 - fi - - # Marcar como completado - touch "$FIRST_RUN_FLAG" - echo "✓ Primera ejecución completada exitosamente" -else - echo "✓ No es primera ejecución, omitiendo setup inicial" -fi - -echo "=== Iniciando PHP-FPM ===" - -# Iniciar PHP-FPM -exec "$@" diff --git a/routes/api.php b/routes/api.php index b752e50..880187a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,23 +1,42 @@ group(function() { - // Tus rutas protegidas + + Route::resource('sim-cards', SimCardController::class); + + Route::resource('packages', PackagesController::class); + + Route::get('clients/search', [ClientController::class, 'search']); + Route::resource('clients', ClientController::class); + + Route::resource('sales', SaleController::class); + + Route::get('cash-closes', [CashCloseController::class, 'index']); + Route::put('cash-closes/close', [CashCloseController::class, 'CloseCashClose']); + Route::get('cash-closes/report', [CashCloseController::class, 'report']); + Route::get('cash-closes/export', [CashCloseController::class, 'exportReport']); }); /** Rutas públicas */