From 87648a53189cb5823de51651c2f5f1d31bc1ccfa Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Tue, 4 Nov 2025 16:40:04 -0600 Subject: [PATCH 01/10] =?UTF-8?q?ADD:=20Creaci=C3=B3n=20de=20migraciones,?= =?UTF-8?q?=20controladores=20y=20modelos=20de=20Paquetes=20y=20Sim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docker/nginx/nginx.conf | 10 +- app/Enums/SimCardStatus.php | 28 +++++ .../Netbien/PackagesController.php | 46 ++++++++ .../Controllers/Netbien/SimCardController.php | 44 ++++++++ .../Requests/Netbien/PackagesStoreRequest.php | 52 +++++++++ .../Netbien/PackagesUpdateRequest.php | 55 +++++++++ .../Requests/Netbien/SimCardStoreRequest.php | 49 ++++++++ .../Requests/Netbien/SimCardUpdateRequest.php | 43 +++++++ app/Models/PackSim.php | 37 ++++++ app/Models/Packages.php | 36 ++++++ app/Models/SimCard.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 +++++ database/seeders/PackagesSeeder.php | 25 +++++ database/seeders/SimCardSeeder.php | 25 +++++ docker-compose.yml | 54 ++++----- dockerfile | 8 +- entrypoint.sh | 105 ------------------ routes/api.php | 12 +- 20 files changed, 603 insertions(+), 150 deletions(-) create mode 100644 app/Enums/SimCardStatus.php create mode 100644 app/Http/Controllers/Netbien/PackagesController.php create mode 100644 app/Http/Controllers/Netbien/SimCardController.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/SimCardStoreRequest.php create mode 100644 app/Http/Requests/Netbien/SimCardUpdateRequest.php create mode 100644 app/Models/PackSim.php create mode 100644 app/Models/Packages.php create mode 100644 app/Models/SimCard.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/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/PackagesController.php b/app/Http/Controllers/Netbien/PackagesController.php new file mode 100644 index 0000000..3057912 --- /dev/null +++ b/app/Http/Controllers/Netbien/PackagesController.php @@ -0,0 +1,46 @@ +paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'data' => $packages, + ]); + } + + public function store(PackagesStoreRequest $request) + { + $validated = $request->validated(); + + $package = Packages::create($validated); + + return ApiResponse::CREATED->response([ + 'data' => $package, + ]); + } + + public function update(PackagesStoreRequest $request, Packages $package) + { + $validated = $request->validated(); + + $package->update($validated); + + return ApiResponse::OK->response([ + 'data' => $package, + ]); + } +} diff --git a/app/Http/Controllers/Netbien/SimCardController.php b/app/Http/Controllers/Netbien/SimCardController.php new file mode 100644 index 0000000..0ae0207 --- /dev/null +++ b/app/Http/Controllers/Netbien/SimCardController.php @@ -0,0 +1,44 @@ +paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'data' => $simCards, + ]); + } + + public function store(SimCardStoreRequest $request) + { + $simCard = SimCard::create($request->validated()); + + return ApiResponse::CREATED->response([ + 'data' => $simCard, + ]); + } + + public function update(SimCardUpdateRequest $request, SimCard $simCard) + { + $simCard->update($request->validated()); + + return ApiResponse::OK->response([ + 'data' => $simCard, + ]); + } + +} diff --git a/app/Http/Requests/Netbien/PackagesStoreRequest.php b/app/Http/Requests/Netbien/PackagesStoreRequest.php new file mode 100644 index 0000000..85c03c3 --- /dev/null +++ b/app/Http/Requests/Netbien/PackagesStoreRequest.php @@ -0,0 +1,52 @@ + + * + * @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', 'numeric', 'min:0'], + 'period' => ['required', 'integer', 'min:1'], + 'data_limit' => ['required', '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.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/PackagesUpdateRequest.php b/app/Http/Requests/Netbien/PackagesUpdateRequest.php new file mode 100644 index 0000000..aecd75a --- /dev/null +++ b/app/Http/Requests/Netbien/PackagesUpdateRequest.php @@ -0,0 +1,55 @@ + + * + * @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' => ['required', 'string', 'max:80'], + 'price' => ['required', 'numeric', 'min:0'], + 'period' => ['required', 'string', 'max:20'], + 'data_limit' => ['required', '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.', + 'period.string' => 'El campo Periodo debe ser una cadena de texto.', + 'period.max' => 'El campo Periodo no debe exceder los 20 caracteres.', + + '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/SimCardStoreRequest.php b/app/Http/Requests/Netbien/SimCardStoreRequest.php new file mode 100644 index 0000000..3bf8090 --- /dev/null +++ b/app/Http/Requests/Netbien/SimCardStoreRequest.php @@ -0,0 +1,49 @@ + + * + * @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'], + ]; + } + + 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.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/SimCardUpdateRequest.php b/app/Http/Requests/Netbien/SimCardUpdateRequest.php new file mode 100644 index 0000000..67763fc --- /dev/null +++ b/app/Http/Requests/Netbien/SimCardUpdateRequest.php @@ -0,0 +1,43 @@ + + * + * @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.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.', + ]; + } +} diff --git a/app/Models/PackSim.php b/app/Models/PackSim.php new file mode 100644 index 0000000..1c43682 --- /dev/null +++ b/app/Models/PackSim.php @@ -0,0 +1,37 @@ + + * + * @version 1.0.0 + */ +class PackSim extends Model +{ + protected $fillable = [ + 'package_id', + 'sim_card_id', + ]; + + protected $casts = [ + 'package_id' => 'integer', + 'sim_card_id' => 'integer', + ]; + + 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..5ac7c7e --- /dev/null +++ b/app/Models/Packages.php @@ -0,0 +1,36 @@ + + * + * @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', + ]; + + public function packSims() + { + return $this->hasMany(PackSim::class, 'package_id'); + } +} diff --git a/app/Models/SimCard.php b/app/Models/SimCard.php new file mode 100644 index 0000000..42550e3 --- /dev/null +++ b/app/Models/SimCard.php @@ -0,0 +1,33 @@ + + * + * @version 1.0.0 + */ +class SimCard extends Model +{ + protected $fillable = [ + 'iccid', + 'msisdn', + 'status', + ]; + + protected $casts = [ + 'status' => SimCardStatus::class, + ]; + + public function packSims() + { + return $this->hasMany(PackSim::class, 'sim_card_id'); + } +} 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..8265ec0 --- /dev/null +++ b/database/migrations/2025_11_04_132448_create_packages_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->integer('price'); + $table->integer('period'); + $table->integer('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/seeders/PackagesSeeder.php b/database/seeders/PackagesSeeder.php new file mode 100644 index 0000000..84d0943 --- /dev/null +++ b/database/seeders/PackagesSeeder.php @@ -0,0 +1,25 @@ + + * + * @version 1.0.0 + */ +class Packages extends Seeder +{ + /** + * Ejecutar sembrado de base de datos + */ + public function run(): void + { + // + } +} diff --git a/database/seeders/SimCardSeeder.php b/database/seeders/SimCardSeeder.php new file mode 100644 index 0000000..b7d01b9 --- /dev/null +++ b/database/seeders/SimCardSeeder.php @@ -0,0 +1,25 @@ + + * + * @version 1.0.0 + */ +class SimCardSeeder extends Seeder +{ + /** + * Ejecutar sembrado de base de datos + */ + public function run(): void + { + // + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 3b1c760..f4e85f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ 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_USERNAME=${DB_USERNAME} @@ -11,27 +11,28 @@ services: - 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 depends_on: mysql: condition: service_healthy - redis: - condition: service_healthy nginx: image: nginx:alpine ports: - - "${NGINX_PORT}:80" + - "${NGINX_PORT:-8080}: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 +42,12 @@ services: MYSQL_PASSWORD: ${DB_PASSWORD} MYSQL_USER: ${DB_USERNAME} ports: - - ${DB_PORT}:${DB_PORT} + - "${DB_PORT:-3306}: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 +59,17 @@ services: PMA_HOST: mysql PMA_PORT: 3306 ports: - - '${PMA_PORT}:80' + - "${PMA_PORT:-8081}: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.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..dc4f2ec 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,23 +1,29 @@ group(function() { - // Tus rutas protegidas + + Route::resource('sim-cards', SimCardController::class); + + Route::resource('packages', PackagesController::class); }); /** Rutas públicas */ -- 2.45.2 From abfa2fe1fd983092068a5b59b50dbf74b1b41863 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Tue, 4 Nov 2025 23:29:34 -0600 Subject: [PATCH 02/10] ADD: Clientes y ventas --- .../Controllers/Netbien/ClientController.php | 59 ++++++ .../Netbien/PackagesController.php | 14 +- .../Controllers/Netbien/SaleController.php | 187 ++++++++++++++++++ .../Controllers/Netbien/SimCardController.php | 33 +++- .../Requests/Netbien/ClientStoreRequest.php | 47 +++++ .../Requests/Netbien/ClientUpdateRequest.php | 47 +++++ .../Requests/Netbien/PackagesStoreRequest.php | 7 +- .../Netbien/PackagesUpdateRequest.php | 10 +- .../Requests/Netbien/SaleStoreRequest.php | 58 ++++++ .../Requests/Netbien/SaleUpdateRequest.php | 41 ++++ .../Requests/Netbien/SimCardStoreRequest.php | 4 + ...uest.php => SimCardUpdateRequest copy.php} | 1 - app/Models/Client.php | 42 ++++ app/Models/ClientSim.php | 36 ++++ app/Models/PackSim.php | 6 + app/Models/Packages.php | 26 +++ app/Models/Sale.php | 34 ++++ app/Models/SaleItem.php | 38 ++++ app/Models/SimCard.php | 27 +++ ...025_11_04_132448_create_packages_table.php | 4 +- ..._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 +++ routes/api.php | 6 + 26 files changed, 860 insertions(+), 22 deletions(-) create mode 100644 app/Http/Controllers/Netbien/ClientController.php create mode 100644 app/Http/Controllers/Netbien/SaleController.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/SaleStoreRequest.php create mode 100644 app/Http/Requests/Netbien/SaleUpdateRequest.php rename app/Http/Requests/Netbien/{SimCardUpdateRequest.php => SimCardUpdateRequest copy.php} (92%) create mode 100644 app/Models/Client.php create mode 100644 app/Models/ClientSim.php create mode 100644 app/Models/Sale.php create mode 100644 app/Models/SaleItem.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 diff --git a/app/Http/Controllers/Netbien/ClientController.php b/app/Http/Controllers/Netbien/ClientController.php new file mode 100644 index 0000000..1b36e60 --- /dev/null +++ b/app/Http/Controllers/Netbien/ClientController.php @@ -0,0 +1,59 @@ +orderBy('id', 'asc')->paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'data' => $clients, + ]); + } + + 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 index 3057912..602378a 100644 --- a/app/Http/Controllers/Netbien/PackagesController.php +++ b/app/Http/Controllers/Netbien/PackagesController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Netbien\PackagesStoreRequest; +use App\Http\Requests\Netbien\PackagesUpdateRequest; use App\Models\Packages; use Notsoweb\ApiResponse\Enums\ApiResponse; @@ -33,14 +34,19 @@ public function store(PackagesStoreRequest $request) ]); } - public function update(PackagesStoreRequest $request, Packages $package) + public function update(PackagesUpdateRequest $request, Packages $package) { - $validated = $request->validated(); - - $package->update($validated); + $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..9f287ae --- /dev/null +++ b/app/Http/Controllers/Netbien/SaleController.php @@ -0,0 +1,187 @@ +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; + } + + $sale = Sale::create([ + 'client_id' => $client->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->items; + + 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 index 0ae0207..dfe213e 100644 --- a/app/Http/Controllers/Netbien/SimCardController.php +++ b/app/Http/Controllers/Netbien/SimCardController.php @@ -7,6 +7,7 @@ use App\Http\Requests\Netbien\SimCardStoreRequest; use App\Http\Requests\Netbien\SimCardUpdateRequest; use App\Models\SimCard; +use Illuminate\Support\Facades\DB; use Notsoweb\ApiResponse\Enums\ApiResponse; /** @@ -16,7 +17,7 @@ class SimCardController extends Controller { public function index() { - $simCards = SimCard::orderBy('id', 'asc')->paginate(config('app.pagination')); + $simCards = SimCard::with('packSims.package:id,name')->orderBy('id', 'asc')->paginate(config('app.pagination')); return ApiResponse::OK->response([ 'data' => $simCards, @@ -25,11 +26,33 @@ public function index() public function store(SimCardStoreRequest $request) { - $simCard = SimCard::create($request->validated()); + try { + DB::beginTransaction(); - return ApiResponse::CREATED->response([ - 'data' => $simCard, - ]); + $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) diff --git a/app/Http/Requests/Netbien/ClientStoreRequest.php b/app/Http/Requests/Netbien/ClientStoreRequest.php new file mode 100644 index 0000000..c7c4661 --- /dev/null +++ b/app/Http/Requests/Netbien/ClientStoreRequest.php @@ -0,0 +1,47 @@ + + * + * @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'], + ]; + } + + 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..53dcf60 --- /dev/null +++ b/app/Http/Requests/Netbien/ClientUpdateRequest.php @@ -0,0 +1,47 @@ + + * + * @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'], + ]; + } + + 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 index 85c03c3..3ee6b81 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', 'numeric', 'min:0'], - 'period' => ['required', 'integer', 'min:1'], - 'data_limit' => ['required', 'integer', 'min:0'], + 'price' => ['required', 'float'], + 'period' => ['required', 'float'], + 'data_limit' => ['required', 'integer'], ]; } @@ -45,7 +45,6 @@ public function messages() : array '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/PackagesUpdateRequest.php b/app/Http/Requests/Netbien/PackagesUpdateRequest.php index aecd75a..8dc4a18 100644 --- a/app/Http/Requests/Netbien/PackagesUpdateRequest.php +++ b/app/Http/Requests/Netbien/PackagesUpdateRequest.php @@ -25,10 +25,10 @@ public function authorize(): bool public function rules(): array { return [ - 'name' => ['required', 'string', 'max:80'], - 'price' => ['required', 'numeric', 'min:0'], - 'period' => ['required', 'string', 'max:20'], - 'data_limit' => ['required', 'integer', 'min:0'], + 'name' => ['sometimes', 'string', 'max:80'], + 'price' => ['sometimes', 'numeric', 'min:0'], + 'period' => ['sometimes', 'numeric', 'min:1'], + 'data_limit' => ['sometimes', 'integer', 'min:0'], ]; } @@ -44,8 +44,6 @@ public function messages() : array 'price.min' => 'El campo Precio no debe ser negativo.', 'period.required' => 'El campo Periodo es obligatorio.', - 'period.string' => 'El campo Periodo debe ser una cadena de texto.', - 'period.max' => 'El campo Periodo no debe exceder los 20 caracteres.', '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.', 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 index 3bf8090..d5bb6cc 100644 --- a/app/Http/Requests/Netbien/SimCardStoreRequest.php +++ b/app/Http/Requests/Netbien/SimCardStoreRequest.php @@ -29,6 +29,7 @@ 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'], ]; } @@ -44,6 +45,9 @@ public function messages() : array '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 copy.php similarity index 92% rename from app/Http/Requests/Netbien/SimCardUpdateRequest.php rename to app/Http/Requests/Netbien/SimCardUpdateRequest copy.php index 67763fc..384b0aa 100644 --- a/app/Http/Requests/Netbien/SimCardUpdateRequest.php +++ b/app/Http/Requests/Netbien/SimCardUpdateRequest copy.php @@ -35,7 +35,6 @@ public function messages() : array { return [ '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.', ]; diff --git a/app/Models/Client.php b/app/Models/Client.php new file mode 100644 index 0000000..f5f766d --- /dev/null +++ b/app/Models/Client.php @@ -0,0 +1,42 @@ + + * + * @version 1.0.0 + */ +class Client extends Model +{ + protected $fillable = [ + 'name', + 'paternal', + 'maternal', + 'email', + 'phone', + ]; + + 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(); + } +} 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 index 1c43682..89209c5 100644 --- a/app/Models/PackSim.php +++ b/app/Models/PackSim.php @@ -18,11 +18,17 @@ 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() diff --git a/app/Models/Packages.php b/app/Models/Packages.php index 5ac7c7e..d601253 100644 --- a/app/Models/Packages.php +++ b/app/Models/Packages.php @@ -29,8 +29,34 @@ class Packages extends Model '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..5949112 --- /dev/null +++ b/app/Models/Sale.php @@ -0,0 +1,34 @@ + + * + * @version 1.0.0 + */ +class Sale extends Model +{ + protected $fillable = [ + 'client_id', + 'total_amount', + 'payment_method', + 'sale_date', + ]; + + public function client() + { + return $this->belongsTo(Client::class); + } + + public function saleItems() + { + return $this->hasMany(SaleItem::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 index 42550e3..1eacd84 100644 --- a/app/Models/SimCard.php +++ b/app/Models/SimCard.php @@ -26,8 +26,35 @@ class SimCard extends Model '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/database/migrations/2025_11_04_132448_create_packages_table.php b/database/migrations/2025_11_04_132448_create_packages_table.php index 8265ec0..41b42f0 100644 --- a/database/migrations/2025_11_04_132448_create_packages_table.php +++ b/database/migrations/2025_11_04_132448_create_packages_table.php @@ -14,9 +14,9 @@ public function up(): void Schema::create('packages', function (Blueprint $table) { $table->id(); $table->string('name'); - $table->integer('price'); + $table->float('price'); $table->integer('period'); - $table->integer('data_limit'); + $table->float('data_limit'); $table->timestamps(); }); } 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/routes/api.php b/routes/api.php index dc4f2ec..ae7a76c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,6 +2,8 @@ use App\Http\Controllers\Netbien\PackagesController; use App\Http\Controllers\Netbien\SimCardController; +use App\Http\Controllers\Netbien\ClientController; +use App\Http\Controllers\Netbien\SaleController; use Illuminate\Support\Facades\Route; /** @@ -24,6 +26,10 @@ Route::resource('sim-cards', SimCardController::class); Route::resource('packages', PackagesController::class); + + Route::resource('clients', ClientController::class); + + Route::resource('sales', SaleController::class); }); /** Rutas públicas */ -- 2.45.2 From db49b127dbd9c624d95446b7b6875af82d9c3dac Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Wed, 5 Nov 2025 16:24:22 -0600 Subject: [PATCH 03/10] ADD: Corte de caja creado --- .../Netbien/CashCloseController.php | 121 ++++++++++++++++++ .../Controllers/Netbien/SaleController.php | 4 + .../Requests/Netbien/ClientStoreRequest.php | 1 + .../Requests/Netbien/ClientUpdateRequest.php | 1 + ...uest copy.php => SimCardUpdateRequest.php} | 0 app/Models/CashClose.php | 85 ++++++++++++ app/Models/Client.php | 1 + app/Models/Sale.php | 6 + app/Services/CashCloseService.php | 31 +++++ ..._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 ++++ database/seeders/ClientSeeder.php | 58 +++++++++ database/seeders/PackageSeeder.php | 25 ++++ database/seeders/PackagesSeeder.php | 51 +++++--- database/seeders/SimCardSeeder.php | 26 +++- docker-compose.yml | 2 +- routes/api.php | 5 + 18 files changed, 486 insertions(+), 21 deletions(-) create mode 100644 app/Http/Controllers/Netbien/CashCloseController.php rename app/Http/Requests/Netbien/{SimCardUpdateRequest copy.php => SimCardUpdateRequest.php} (100%) create mode 100644 app/Models/CashClose.php create mode 100644 app/Services/CashCloseService.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/seeders/ClientSeeder.php create mode 100644 database/seeders/PackageSeeder.php diff --git a/app/Http/Controllers/Netbien/CashCloseController.php b/app/Http/Controllers/Netbien/CashCloseController.php new file mode 100644 index 0000000..be47db7 --- /dev/null +++ b/app/Http/Controllers/Netbien/CashCloseController.php @@ -0,0 +1,121 @@ +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($id) + { + $today = now()->format('Y-m-d'); + + $cashClose = CashClose::findOrFail($id); + + $totalSales = Sale::where('cash_close_id', $id)->sum('total_amount'); + $paymentMethods = Sale::where('cash_close_id', $id) + ->select('payment_method', DB::raw('SUM(total_amount) as total')) + ->groupBy('payment_method') + ->get(); + + $cashClose->update([ + 'income' => $totalSales, + 'balance' => $totalSales - $cashClose->exit, + 'payment_methods' => $paymentMethods, + 'status' => 'closed', + 'close_date' => $today, + ]); + + return ApiResponse::OK->response([ + 'message' => 'Corte de caja cerrado exitosamente.', + 'data' => $cashClose, + ]); + } + + public function report($id) + { + // Información del corte de caja + $cashClose = CashClose::with('user:id,name') + ->withCount('sales') + ->findOrFail($id); + + // 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') + ->where('sales.cash_close_id', $id) + ->select( + 'packages.name as paquete', + DB::raw('COUNT(*) as total_vendidos') + ) + ->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') + ->where('sales.cash_close_id', $id) + ->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 = DB::table('sales') + ->join('clients', 'sales.client_id', '=', 'clients.id') + ->join('sale_items', 'sales.id', '=', 'sale_items.sale_id') + ->join('packages', 'sale_items.package_id', '=', 'packages.id') + ->join('sim_cards', 'sale_items.sim_card_id', '=', 'sim_cards.id') + ->where('sales.cash_close_id', $id) + ->select( + DB::raw("CONCAT(clients.name, ' ', clients.paternal, ' ', clients.maternal) as nombre_comprador"), + 'sim_cards.iccid as id_sim', + 'sim_cards.msisdn as numero_asignado', + 'packages.name as paquete', + 'packages.price as costo', + 'sales.payment_method as medio_pago' + ) + ->orderBy('sales.id', 'desc') + ->get(); + + return ApiResponse::OK->response([ + 'cash_close' => $cashClose, + 'ventas por paquete' => $packageStats, + 'ventas por duracion' => $durationStats, + 'ventas detalladas' => $detailedSales, + ]); + } +} diff --git a/app/Http/Controllers/Netbien/SaleController.php b/app/Http/Controllers/Netbien/SaleController.php index 9f287ae..78234dd 100644 --- a/app/Http/Controllers/Netbien/SaleController.php +++ b/app/Http/Controllers/Netbien/SaleController.php @@ -11,6 +11,7 @@ use App\Enums\SimCardStatus; use App\Http\Requests\Netbien\SaleStoreRequest; use App\Http\Requests\Netbien\SaleUpdateRequest; +use App\Services\CashCloseService; use Illuminate\Support\Facades\DB; use Notsoweb\ApiResponse\Enums\ApiResponse; @@ -66,8 +67,11 @@ public function store(SaleStoreRequest $request) $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(), diff --git a/app/Http/Requests/Netbien/ClientStoreRequest.php b/app/Http/Requests/Netbien/ClientStoreRequest.php index c7c4661..2bfaee5 100644 --- a/app/Http/Requests/Netbien/ClientStoreRequest.php +++ b/app/Http/Requests/Netbien/ClientStoreRequest.php @@ -32,6 +32,7 @@ public function rules(): array 'maternal' => ['required', 'string'], 'email' => ['nullable', 'email'], 'phone' => ['nullable', 'string', 'max:10'], + 'rfc' => ['required', 'string', 'max:13'], ]; } diff --git a/app/Http/Requests/Netbien/ClientUpdateRequest.php b/app/Http/Requests/Netbien/ClientUpdateRequest.php index 53dcf60..f81c367 100644 --- a/app/Http/Requests/Netbien/ClientUpdateRequest.php +++ b/app/Http/Requests/Netbien/ClientUpdateRequest.php @@ -32,6 +32,7 @@ public function rules(): array 'maternal' => ['sometimes', 'string', 'max:100'], 'email' => ['sometimes', 'email'], 'phone' => ['nullable', 'string', 'max:20'], + 'rfc' => ['sometimes', 'string', 'max:13'], ]; } diff --git a/app/Http/Requests/Netbien/SimCardUpdateRequest copy.php b/app/Http/Requests/Netbien/SimCardUpdateRequest.php similarity index 100% rename from app/Http/Requests/Netbien/SimCardUpdateRequest copy.php rename to app/Http/Requests/Netbien/SimCardUpdateRequest.php diff --git a/app/Models/CashClose.php b/app/Models/CashClose.php new file mode 100644 index 0000000..deb26de --- /dev/null +++ b/app/Models/CashClose.php @@ -0,0 +1,85 @@ + 'decimal:2', + 'income' => 'decimal:2', + 'exit' => 'decimal:2', + 'expected_balance' => 'decimal:2', + 'difference' => 'decimal:2', + 'close_date' => 'date', + ]; + + /** + * 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'); + } + + /** + * Scope para cortes de una fecha específica + */ + public function scopeByDate($query, $date) + { + return $query->whereDate('close_date', $date); + } + + /** + * Calcula automáticamente el balance + */ + public function calculateBalance() + { + $this->balance = $this->income - $this->exit; + + if ($this->expected_balance) { + $this->difference = $this->balance - $this->expected_balance; + } + + return $this->balance; + } +} diff --git a/app/Models/Client.php b/app/Models/Client.php index f5f766d..4d458b4 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -21,6 +21,7 @@ class Client extends Model 'maternal', 'email', 'phone', + 'rfc', ]; public function sales() diff --git a/app/Models/Sale.php b/app/Models/Sale.php index 5949112..3e83c78 100644 --- a/app/Models/Sale.php +++ b/app/Models/Sale.php @@ -17,6 +17,7 @@ class Sale extends Model { protected $fillable = [ 'client_id', + 'cash_close_id', 'total_amount', 'payment_method', 'sale_date', @@ -31,4 +32,9 @@ public function saleItems() { return $this->hasMany(SaleItem::class); } + + public function cashClose() + { + return $this->belongsTo(CashClose::class); + } } diff --git a/app/Services/CashCloseService.php b/app/Services/CashCloseService.php new file mode 100644 index 0000000..5595bb5 --- /dev/null +++ b/app/Services/CashCloseService.php @@ -0,0 +1,31 @@ +format('Y-m-d'); + $cashClose = CashClose::open()->byDate($today)->first(); + + if (!$cashClose) { + $cashClose = CashClose::create([ + 'close_date' => $today, + 'income' => 0, + 'exit' => 0, + 'balance' => 0, + 'status' => 'open', + 'user_id' => Auth::id(), + ]); + } + + return $cashClose; + } +} 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..e666632 --- /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', 'reviewed'])->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/seeders/ClientSeeder.php b/database/seeders/ClientSeeder.php new file mode 100644 index 0000000..e6eb3f7 --- /dev/null +++ b/database/seeders/ClientSeeder.php @@ -0,0 +1,58 @@ + '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', + ], + [ + 'name' => 'Carlos', + 'paternal' => 'Sánchez', + 'maternal' => 'Ruiz', + 'email' => 'carlos.sanchez@example.com', + 'phone' => '555-1004', + ], + [ + 'name' => 'Laura', + 'paternal' => 'Gómez', + 'maternal' => 'Flores', + 'email' => 'laura.gomez@example.com', + 'phone' => '555-1005', + ] + ]; + + foreach ($clients as $client) { + Client::create($client); + } + } +} 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 index 84d0943..07b8521 100644 --- a/database/seeders/PackagesSeeder.php +++ b/database/seeders/PackagesSeeder.php @@ -1,25 +1,42 @@ - - * - * @version 1.0.0 - */ -class Packages extends Seeder +class PackagesSeeder extends Seeder { - /** - * Ejecutar sembrado de base de datos - */ public function run(): void { - // + $packages = [ + [ + 'name' => '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 index b7d01b9..26f49e4 100644 --- a/database/seeders/SimCardSeeder.php +++ b/database/seeders/SimCardSeeder.php @@ -3,14 +3,15 @@ * @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved */ +use App\Models\SimCard; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; /** * Descripción - * + * * @author Moisés Cortés C. - * + * * @version 1.0.0 */ class SimCardSeeder extends Seeder @@ -20,6 +21,25 @@ class SimCardSeeder extends Seeder */ 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 f4e85f1..073d101 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: 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} diff --git a/routes/api.php b/routes/api.php index ae7a76c..1b2a729 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ Date: Wed, 5 Nov 2025 20:25:26 -0600 Subject: [PATCH 04/10] Corte de caja correcciones --- .../Netbien/CashCloseController.php | 47 ++++++++++--------- app/Models/CashClose.php | 5 -- app/Models/Client.php | 8 ++++ 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/app/Http/Controllers/Netbien/CashCloseController.php b/app/Http/Controllers/Netbien/CashCloseController.php index be47db7..f656cc7 100644 --- a/app/Http/Controllers/Netbien/CashCloseController.php +++ b/app/Http/Controllers/Netbien/CashCloseController.php @@ -35,7 +35,7 @@ public function index(Request $request) ]); } - public function CloseCashClose($id) + public function closeCashClose($id) { $today = now()->format('Y-m-d'); @@ -50,7 +50,6 @@ public function CloseCashClose($id) $cashClose->update([ 'income' => $totalSales, 'balance' => $totalSales - $cashClose->exit, - 'payment_methods' => $paymentMethods, 'status' => 'closed', 'close_date' => $today, ]); @@ -94,28 +93,34 @@ public function report($id) ->get(); // Reporte detallado de ventas - $detailedSales = DB::table('sales') - ->join('clients', 'sales.client_id', '=', 'clients.id') - ->join('sale_items', 'sales.id', '=', 'sale_items.sale_id') - ->join('packages', 'sale_items.package_id', '=', 'packages.id') - ->join('sim_cards', 'sale_items.sim_card_id', '=', 'sim_cards.id') - ->where('sales.cash_close_id', $id) - ->select( - DB::raw("CONCAT(clients.name, ' ', clients.paternal, ' ', clients.maternal) as nombre_comprador"), - 'sim_cards.iccid as id_sim', - 'sim_cards.msisdn as numero_asignado', - 'packages.name as paquete', - 'packages.price as costo', - 'sales.payment_method as medio_pago' - ) - ->orderBy('sales.id', 'desc') - ->get(); + $detailedSales = SaleItem::whereHas('sale', function($query) use ($id) { + $query->where('cash_close_id', $id); + }) + ->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 + ]; + }); + return ApiResponse::OK->response([ 'cash_close' => $cashClose, - 'ventas por paquete' => $packageStats, - 'ventas por duracion' => $durationStats, - 'ventas detalladas' => $detailedSales, + 'ventas_paquete' => $packageStats, + 'ventas_duracion' => $durationStats, + 'ventas_detalladas' => $detailedSales, ]); } } diff --git a/app/Models/CashClose.php b/app/Models/CashClose.php index deb26de..a75072a 100644 --- a/app/Models/CashClose.php +++ b/app/Models/CashClose.php @@ -12,10 +12,7 @@ class CashClose extends Model 'balance', 'income', 'exit', - 'expected_balance', - 'difference', 'close_date', - 'notes', 'status', 'user_id', ]; @@ -24,8 +21,6 @@ class CashClose extends Model 'balance' => 'decimal:2', 'income' => 'decimal:2', 'exit' => 'decimal:2', - 'expected_balance' => 'decimal:2', - 'difference' => 'decimal:2', 'close_date' => 'date', ]; diff --git a/app/Models/Client.php b/app/Models/Client.php index 4d458b4..df3baa0 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -4,6 +4,7 @@ */ +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; /** @@ -40,4 +41,11 @@ public function simCards() ->withPivot('assigned_at', 'released_at', 'is_active') ->withTimestamps(); } + + public function fullName(): Attribute + { + return Attribute::make( + get: fn () => $this->name . ' ' . $this->paternal . ' ' . $this->maternal, + ); + } } -- 2.45.2 From faea7075022631e9317172f34251cec0f38378d6 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Thu, 6 Nov 2025 11:00:50 -0600 Subject: [PATCH 05/10] FIX: SaleController arreglado metodo destroy --- .../Controllers/Netbien/SaleController.php | 2 +- .../Controllers/Netbien/SimCardController.php | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Netbien/SaleController.php b/app/Http/Controllers/Netbien/SaleController.php index 78234dd..5f3bb4a 100644 --- a/app/Http/Controllers/Netbien/SaleController.php +++ b/app/Http/Controllers/Netbien/SaleController.php @@ -145,7 +145,7 @@ public function destroy(Sale $sale) DB::beginTransaction(); // Obtener todos los items de la venta - $items = $sale->items; + $items = $sale->saleItems; foreach ($items as $item) { $sim = $item->simCard; diff --git a/app/Http/Controllers/Netbien/SimCardController.php b/app/Http/Controllers/Netbien/SimCardController.php index dfe213e..f366ed7 100644 --- a/app/Http/Controllers/Netbien/SimCardController.php +++ b/app/Http/Controllers/Netbien/SimCardController.php @@ -64,4 +64,24 @@ public function update(SimCardUpdateRequest $request, SimCard $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(), + ]); + } + } + } -- 2.45.2 From 5270b34becda62025ae150cc5316e8f7ffa2875c Mon Sep 17 00:00:00 2001 From: "juan.zapata" Date: Fri, 7 Nov 2025 08:54:13 -0600 Subject: [PATCH 06/10] FIX: Package crear arreglado --- .../Requests/Netbien/PackagesStoreRequest.php | 4 +- docker-compose.yml | 70 ++++++++++--------- entrypoint-dev.sh | 10 +-- 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/app/Http/Requests/Netbien/PackagesStoreRequest.php b/app/Http/Requests/Netbien/PackagesStoreRequest.php index 3ee6b81..fc8ddad 100644 --- a/app/Http/Requests/Netbien/PackagesStoreRequest.php +++ b/app/Http/Requests/Netbien/PackagesStoreRequest.php @@ -26,8 +26,8 @@ public function rules(): array { return [ 'name' => ['required', 'string', 'max:80'], - 'price' => ['required', 'float'], - 'period' => ['required', 'float'], + 'price' => ['required', 'integer'], + 'period' => ['required', 'integer'], 'data_limit' => ['required', 'integer'], ]; } diff --git a/docker-compose.yml b/docker-compose.yml index 073d101..eda8317 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,14 +17,16 @@ services: networks: - netbien-network mem_limit: 512m - depends_on: - mysql: - condition: service_healthy + extra_hosts: + - "host.docker.internal:host-gateway" +# depends_on: +# mysql: +# condition: service_healthy nginx: image: nginx:alpine ports: - - "${NGINX_PORT:-8080}:80" + - "${NGINX_PORT}:80" volumes: - ./:/var/www/netbien - ./Docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf @@ -34,37 +36,37 @@ services: depends_on: - netbien-backend - mysql: - image: mysql:8.0 - environment: - MYSQL_DATABASE: ${DB_DATABASE} - MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} - MYSQL_PASSWORD: ${DB_PASSWORD} - MYSQL_USER: ${DB_USERNAME} - ports: - - "${DB_PORT:-3306}:3306" - volumes: - - mysql_data:/var/lib/mysql - networks: - - netbien-network - mem_limit: 512m - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - timeout: 15s - retries: 10 +# mysql: +# image: mysql:8.0 +# environment: +# MYSQL_DATABASE: ${DB_DATABASE} +# MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} +# MYSQL_PASSWORD: ${DB_PASSWORD} +# MYSQL_USER: ${DB_USERNAME} +# ports: +# - "${DB_PORT}:3306" +# volumes: +# - mysql_data:/var/lib/mysql +# networks: +# - netbien-network +# mem_limit: 512m +# healthcheck: +# test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] +# timeout: 15s +# retries: 10 - phpmyadmin: - image: phpmyadmin/phpmyadmin - environment: - PMA_HOST: mysql - PMA_PORT: 3306 - ports: - - "${PMA_PORT:-8081}:80" - depends_on: - - mysql - networks: - - netbien-network - mem_limit: 512m +# phpmyadmin: +# image: phpmyadmin/phpmyadmin +# environment: +# PMA_HOST: mysql +# PMA_PORT: 3306 +# ports: +# - "${PMA_PORT}:80" +# depends_on: +# - mysql +# networks: +# - netbien-network +# mem_limit: 512m volumes: mysql_data: 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 -- 2.45.2 From 29eac5ab00f4aba705574a5de2b6ff9aacf6ccb9 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Fri, 7 Nov 2025 09:06:12 -0600 Subject: [PATCH 07/10] ADD: metodo show client, packages, simcard --- app/Http/Controllers/Netbien/ClientController.php | 9 +++++++++ app/Http/Controllers/Netbien/PackagesController.php | 7 +++++++ app/Http/Controllers/Netbien/SimCardController.php | 9 +++++++++ 3 files changed, 25 insertions(+) diff --git a/app/Http/Controllers/Netbien/ClientController.php b/app/Http/Controllers/Netbien/ClientController.php index 1b36e60..76d4453 100644 --- a/app/Http/Controllers/Netbien/ClientController.php +++ b/app/Http/Controllers/Netbien/ClientController.php @@ -21,6 +21,15 @@ public function index() ]); } + 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()); diff --git a/app/Http/Controllers/Netbien/PackagesController.php b/app/Http/Controllers/Netbien/PackagesController.php index 602378a..8bbd1d0 100644 --- a/app/Http/Controllers/Netbien/PackagesController.php +++ b/app/Http/Controllers/Netbien/PackagesController.php @@ -23,6 +23,13 @@ public function index() ]); } + public function show(Packages $package) + { + return ApiResponse::OK->response([ + 'data' => $package, + ]); + } + public function store(PackagesStoreRequest $request) { $validated = $request->validated(); diff --git a/app/Http/Controllers/Netbien/SimCardController.php b/app/Http/Controllers/Netbien/SimCardController.php index f366ed7..a99b778 100644 --- a/app/Http/Controllers/Netbien/SimCardController.php +++ b/app/Http/Controllers/Netbien/SimCardController.php @@ -24,6 +24,15 @@ public function index() ]); } + public function show(SimCard $simCard) + { + $simCard->load('packSims.package:id,name'); + + return ApiResponse::OK->response([ + 'data' => $simCard, + ]); + } + public function store(SimCardStoreRequest $request) { try { -- 2.45.2 From 4d6059a1e90320144dd6dcc1e87cfebe09cd1e50 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Fri, 7 Nov 2025 17:33:39 -0600 Subject: [PATCH 08/10] Corte de caja actualizado --- .../Netbien/CashCloseController.php | 50 +++++++++++--- .../Controllers/Netbien/ClientController.php | 23 +++++++ app/Models/CashClose.php | 44 ++++--------- app/Services/CashCloseService.php | 12 ++-- ..._11_05_114844_create_cash_closes_table.php | 2 +- ...fy_clients_table_rfc_email_constraints.php | 24 +++++++ ...831_modify_cash_closes_table_structure.php | 55 ++++++++++++++++ database/seeders/ClientSeeder.php | 3 + database/seeders/DevSeeder.php | 8 ++- docker-compose.yml | 66 +++++++++---------- routes/api.php | 3 +- 11 files changed, 208 insertions(+), 82 deletions(-) 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 diff --git a/app/Http/Controllers/Netbien/CashCloseController.php b/app/Http/Controllers/Netbien/CashCloseController.php index f656cc7..a899e57 100644 --- a/app/Http/Controllers/Netbien/CashCloseController.php +++ b/app/Http/Controllers/Netbien/CashCloseController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Models\CashClose; use App\Models\Sale; +use App\Models\SaleItem; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Notsoweb\ApiResponse\Enums\ApiResponse; @@ -35,28 +36,59 @@ public function index(Request $request) ]); } - public function closeCashClose($id) + public function closeCashClose(Request $request) { - $today = now()->format('Y-m-d'); + $request ->validate([ + 'exit' => 'sometimes|numeric|min:0', + ]); - $cashClose = CashClose::findOrFail($id); + $cashClose = CashClose::open()->first(); - $totalSales = Sale::where('cash_close_id', $id)->sum('total_amount'); - $paymentMethods = Sale::where('cash_close_id', $id) + 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') - ->get(); + ->pluck('total', 'payment_method'); + + $exit = $request->input('exit', 0); $cashClose->update([ + 'closed_at' => now(), 'income' => $totalSales, - 'balance' => $totalSales - $cashClose->exit, + 'exit' => $exit, + 'income_cash' => $paymentMethods->get('cash', 0), + 'income_card' => $paymentMethods->get('card', 0), + 'income_transfer' => $paymentMethods->get('transfer', 0), 'status' => 'closed', - 'close_date' => $today, ]); + $balanceFinal = $cashClose->initial_balance + $totalSales - $exit; + return ApiResponse::OK->response([ 'message' => 'Corte de caja cerrado exitosamente.', - 'data' => $cashClose, + '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, + ], + ], ]); } diff --git a/app/Http/Controllers/Netbien/ClientController.php b/app/Http/Controllers/Netbien/ClientController.php index 76d4453..c85d687 100644 --- a/app/Http/Controllers/Netbien/ClientController.php +++ b/app/Http/Controllers/Netbien/ClientController.php @@ -21,6 +21,29 @@ public function index() ]); } + 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'); diff --git a/app/Models/CashClose.php b/app/Models/CashClose.php index a75072a..3ca6cef 100644 --- a/app/Models/CashClose.php +++ b/app/Models/CashClose.php @@ -1,27 +1,31 @@ 'decimal:2', + 'initial_balance' => 'decimal:2', 'income' => 'decimal:2', 'exit' => 'decimal:2', - 'close_date' => 'date', + 'income_cash' => 'decimal:2', + 'income_card' => 'decimal:2', + 'income_transfer' => 'decimal:2', + 'opened_at' => 'datetime', + 'closed_at' => 'datetime', ]; /** @@ -55,26 +59,4 @@ public function scopeClosed($query) { return $query->where('status', 'closed'); } - - /** - * Scope para cortes de una fecha específica - */ - public function scopeByDate($query, $date) - { - return $query->whereDate('close_date', $date); - } - - /** - * Calcula automáticamente el balance - */ - public function calculateBalance() - { - $this->balance = $this->income - $this->exit; - - if ($this->expected_balance) { - $this->difference = $this->balance - $this->expected_balance; - } - - return $this->balance; - } } diff --git a/app/Services/CashCloseService.php b/app/Services/CashCloseService.php index 5595bb5..7a9f5f4 100644 --- a/app/Services/CashCloseService.php +++ b/app/Services/CashCloseService.php @@ -12,17 +12,19 @@ class CashCloseService */ public static function getOrCreateOpenCashClose() { - $today = now()->format('Y-m-d'); - $cashClose = CashClose::open()->byDate($today)->first(); + $cashClose = CashClose::open()->first(); if (!$cashClose) { $cashClose = CashClose::create([ - 'close_date' => $today, + 'user_id' => Auth::id(), + 'opened_at' => now(), + 'initial_balance' => 0, 'income' => 0, 'exit' => 0, - 'balance' => 0, + 'income_cash' => 0, + 'income_card' => 0, + 'income_transfer' => 0, 'status' => 'open', - 'user_id' => Auth::id(), ]); } 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 index e666632..b56d019 100644 --- a/database/migrations/2025_11_05_114844_create_cash_closes_table.php +++ b/database/migrations/2025_11_05_114844_create_cash_closes_table.php @@ -17,7 +17,7 @@ public function up(): void $table->decimal('balance', 10, 2); $table->decimal('income', 10, 2); $table->decimal('exit', 10, 2)->default(0); - $table->enum('status', ['open', 'closed', 'reviewed'])->default('open')->comment('Estado del corte'); + $table->enum('status', ['open', 'closed'])->default('open')->comment('Estado del corte'); $table->timestamp('close_date'); $table->timestamps(); }); 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 index e6eb3f7..6fbbe01 100644 --- a/database/seeders/ClientSeeder.php +++ b/database/seeders/ClientSeeder.php @@ -34,6 +34,7 @@ public function run(): void 'maternal' => 'Cruz', 'email' => 'maria.hernandez@example.com', 'phone' => '555-1003', + 'rfc' => 'MAHC910203XXX', ], [ 'name' => 'Carlos', @@ -41,6 +42,7 @@ public function run(): void 'maternal' => 'Ruiz', 'email' => 'carlos.sanchez@example.com', 'phone' => '555-1004', + 'rfc' => 'CASR910203XXX', ], [ 'name' => 'Laura', @@ -48,6 +50,7 @@ public function run(): void 'maternal' => 'Flores', 'email' => 'laura.gomez@example.com', 'phone' => '555-1005', + 'rfc' => 'LAGF910203XXX', ] ]; 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/docker-compose.yml b/docker-compose.yml index eda8317..ecdad7e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,9 +19,9 @@ services: mem_limit: 512m extra_hosts: - "host.docker.internal:host-gateway" -# depends_on: -# mysql: -# condition: service_healthy + depends_on: + mysql: + condition: service_healthy nginx: image: nginx:alpine @@ -36,37 +36,37 @@ services: depends_on: - netbien-backend -# mysql: -# image: mysql:8.0 -# environment: -# MYSQL_DATABASE: ${DB_DATABASE} -# MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} -# MYSQL_PASSWORD: ${DB_PASSWORD} -# MYSQL_USER: ${DB_USERNAME} -# ports: -# - "${DB_PORT}:3306" -# volumes: -# - mysql_data:/var/lib/mysql -# networks: -# - netbien-network -# mem_limit: 512m -# healthcheck: -# test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] -# timeout: 15s -# retries: 10 + mysql: + image: mysql:8.0 + environment: + MYSQL_DATABASE: ${DB_DATABASE} + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_PASSWORD: ${DB_PASSWORD} + MYSQL_USER: ${DB_USERNAME} + ports: + - "${DB_PORT}:3306" + volumes: + - mysql_data:/var/lib/mysql + networks: + - netbien-network + mem_limit: 512m + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 15s + retries: 10 -# phpmyadmin: -# image: phpmyadmin/phpmyadmin -# environment: -# PMA_HOST: mysql -# PMA_PORT: 3306 -# ports: -# - "${PMA_PORT}:80" -# depends_on: -# - mysql -# networks: -# - netbien-network -# mem_limit: 512m + phpmyadmin: + image: phpmyadmin/phpmyadmin + environment: + PMA_HOST: mysql + PMA_PORT: 3306 + ports: + - "${PMA_PORT}:80" + depends_on: + - mysql + networks: + - netbien-network + mem_limit: 512m volumes: mysql_data: diff --git a/routes/api.php b/routes/api.php index 1b2a729..5f3e2c9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -28,12 +28,13 @@ 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/{id}/close', [CashCloseController::class, 'CloseCashClose']); + Route::put('cash-closes/close', [CashCloseController::class, 'CloseCashClose']); Route::get('cash-closes/{id}/report', [CashCloseController::class, 'report']); }); -- 2.45.2 From e679bcaedfbd400cd212207f53c205fb613ec431 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Mon, 10 Nov 2025 10:09:59 -0600 Subject: [PATCH 09/10] FIX: Reporte de corte de caja por rango de fecha --- .../Netbien/CashCloseController.php | 90 ++++++++++++++----- routes/api.php | 2 +- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/app/Http/Controllers/Netbien/CashCloseController.php b/app/Http/Controllers/Netbien/CashCloseController.php index a899e57..c6220ec 100644 --- a/app/Http/Controllers/Netbien/CashCloseController.php +++ b/app/Http/Controllers/Netbien/CashCloseController.php @@ -9,6 +9,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Notsoweb\ApiResponse\Enums\ApiResponse; +use phpseclib3\Crypt\RC2; /** * @@ -38,21 +39,21 @@ public function index(Request $request) public function closeCashClose(Request $request) { - $request ->validate([ - 'exit' => 'sometimes|numeric|min:0', - ]); + $request->validate([ + 'exit' => 'sometimes|numeric|min:0', + ]); - $cashClose = CashClose::open()->first(); + $cashClose = CashClose::open()->first(); - if(!$cashClose) { - return ApiResponse::NOT_FOUND->response([ - 'message' => 'No hay un corte de caja abierto para cerrar.', - ]); - } + 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'); + $totalSales = Sale::where('cash_close_id', $cashClose->id)->sum('total_amount'); - $paymentMethods = Sale::where('cash_close_id', $cashClose->id) + $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'); @@ -92,21 +93,45 @@ public function closeCashClose(Request $request) ]); } - public function report($id) + 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 - $cashClose = CashClose::with('user:id,name') - ->withCount('sales') - ->findOrFail($id); + $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') - ->where('sales.cash_close_id', $id) + ->whereIn('sales.cash_close_id', $cashCloseIds) ->select( 'packages.name as paquete', - DB::raw('COUNT(*) as total_vendidos') + DB::raw('COUNT(*) as total_vendidos'), + DB::raw('SUM(packages.price) as total_ingresos') ) ->groupBy('packages.id', 'packages.name') ->get(); @@ -115,7 +140,7 @@ public function report($id) $durationStats = DB::table('sale_items') ->join('sales', 'sale_items.sale_id', '=', 'sales.id') ->join('packages', 'sale_items.package_id', '=', 'packages.id') - ->where('sales.cash_close_id', $id) + ->whereIn('sales.cash_close_id', $cashCloseIds) ->select( 'packages.period as duracion_dias', DB::raw('COUNT(DISTINCT sales.id) as total_ventas') @@ -125,9 +150,9 @@ public function report($id) ->get(); // Reporte detallado de ventas - $detailedSales = SaleItem::whereHas('sale', function($query) use ($id) { - $query->where('cash_close_id', $id); - }) + $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', @@ -136,7 +161,7 @@ public function report($id) ]) ->orderBy('id', 'asc') ->paginate(config('app.pagination')) - ->through(function($item) { + ->through(function ($item) { return [ 'nombre_comprador' => $item->sale->client->full_name, 'id_sim' => $item->simCard->iccid, @@ -147,9 +172,28 @@ public function report($id) ]; }); + $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_close' => $cashClose, + '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, diff --git a/routes/api.php b/routes/api.php index 5f3e2c9..51fd845 100644 --- a/routes/api.php +++ b/routes/api.php @@ -35,7 +35,7 @@ Route::get('cash-closes', [CashCloseController::class, 'index']); Route::put('cash-closes/close', [CashCloseController::class, 'CloseCashClose']); - Route::get('cash-closes/{id}/report', [CashCloseController::class, 'report']); + Route::get('cash-closes/report', [CashCloseController::class, 'report']); }); /** Rutas públicas */ -- 2.45.2 From c342fb7764a13b3d730ac81222bc93bc611cac45 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Mon, 10 Nov 2025 16:43:48 -0600 Subject: [PATCH 10/10] ADD: Reporte en excel --- .../Netbien/CashCloseController.php | 94 +++++++++++++++++++ routes/api.php | 1 + 2 files changed, 95 insertions(+) diff --git a/app/Http/Controllers/Netbien/CashCloseController.php b/app/Http/Controllers/Netbien/CashCloseController.php index c6220ec..34fa184 100644 --- a/app/Http/Controllers/Netbien/CashCloseController.php +++ b/app/Http/Controllers/Netbien/CashCloseController.php @@ -199,4 +199,98 @@ public function report(Request $request) '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/routes/api.php b/routes/api.php index 51fd845..880187a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -36,6 +36,7 @@ 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 */ -- 2.45.2