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] 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 @@