From 599bb68ce6fd95071426f27535ae231a1306d4e4 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Wed, 31 Dec 2025 13:45:42 -0600 Subject: [PATCH] ADD: Corte de caja --- .../Controllers/CashRegisterController.php | 138 ++++++++++++++++++ app/Models/CashRegister.php | 62 ++++++++ app/Models/Sale.php | 6 + app/Services/CashRegisterService.php | 95 ++++++++++++ app/Services/SaleService.php | 10 ++ ..._31_132746_create_cash_registers_table.php | 44 ++++++ routes/api.php | 7 + 7 files changed, 362 insertions(+) create mode 100644 app/Http/Controllers/CashRegisterController.php create mode 100644 app/Models/CashRegister.php create mode 100644 app/Services/CashRegisterService.php create mode 100644 database/migrations/2025_12_31_132746_create_cash_registers_table.php diff --git a/app/Http/Controllers/CashRegisterController.php b/app/Http/Controllers/CashRegisterController.php new file mode 100644 index 0000000..1214087 --- /dev/null +++ b/app/Http/Controllers/CashRegisterController.php @@ -0,0 +1,138 @@ +orderBy('opened_at', 'desc'); + + // Filtro por rango de fechas + if ($request->has('from') && $request->has('to')) { + $query->whereBetween('opened_at', [$request->from, $request->to]); + } + + // Filtro por estado + if ($request->has('status')) { + $query->where('status', $request->status); + } + + $registers = $query->paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'registers' => $registers + ]); + } + + /** + * Ver caja actual del usuario + */ + public function current() + { + $register = CashRegister::where('user_id', auth()->id()) + ->where('status', 'open') + ->with(['user', 'sales']) + ->first(); + + if (!$register) { + return ApiResponse::OK->response([ + 'register' => null, + 'message' => 'No tienes una caja abierta.' + ]); + } + + $summary = $this->cashRegisterService->getCurrentSummary($register); + + return ApiResponse::OK->response($summary); + } + + /** + * Ver detalle de un corte específico + */ + public function show(CashRegister $register) + { + $register->load(['user', 'sales.details']); + + $summary = [ + 'register' => $register, + 'payment_summary' => $register->getTotalsByPaymentMethod(), + ]; + + return ApiResponse::OK->response($summary); + } + + /** + * Abrir caja + */ + public function open(Request $request) + { + $request->validate([ + 'initial_cash' => ['required', 'numeric', 'min:0'], + ], [ + 'initial_cash.required' => 'El efectivo inicial es obligatorio.', + 'initial_cash.numeric' => 'El efectivo inicial debe ser un número.', + 'initial_cash.min' => 'El efectivo inicial no puede ser negativo.', + ]); + + try { + $register = $this->cashRegisterService->openRegister([ + 'user_id' => auth()->id(), + 'initial_cash' => $request->initial_cash, + ]); + + return ApiResponse::CREATED->response([ + 'model' => $register, + 'message' => 'Caja abierta exitosamente.' + ]); + } catch (\Exception $e) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => $e->getMessage() + ]); + } + } + + /** + * Cerrar caja + */ + public function close(CashRegister $register, Request $request) + { + $request->validate([ + 'final_cash' => ['required', 'numeric', 'min:0'], + 'notes' => ['nullable', 'string', 'max:500'], + ], [ + 'final_cash.required' => 'El efectivo final es obligatorio.', + 'final_cash.numeric' => 'El efectivo final debe ser un número.', + 'final_cash.min' => 'El efectivo final no puede ser negativo.', + ]); + + try { + $closedRegister = $this->cashRegisterService->closeRegister($register, [ + 'final_cash' => $request->final_cash, + 'notes' => $request->notes, + ]); + + return ApiResponse::OK->response([ + 'model' => $closedRegister, + 'message' => 'Caja cerrada exitosamente.' + ]); + } catch (\Exception $e) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => $e->getMessage() + ]); + } + } +} diff --git a/app/Models/CashRegister.php b/app/Models/CashRegister.php new file mode 100644 index 0000000..34fc908 --- /dev/null +++ b/app/Models/CashRegister.php @@ -0,0 +1,62 @@ + 'datetime', + 'closed_at' => 'datetime', + 'initial_cash' => 'decimal:2', + 'final_cash' => 'decimal:2', + 'expected_cash' => 'decimal:2', + 'difference' => 'decimal:2', + 'total_sales' => 'decimal:2', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } + + public function sales() + { + return $this->hasMany(Sale::class); + } + + /** + * Verificar si la caja está abierta + */ + public function isOpen(): bool + { + return $this->status === 'open'; + } + + /** + * Calcular totales por método de pago + */ + public function getTotalsByPaymentMethod() + { + return $this->sales() + ->where('status', 'completed') + ->selectRaw('payment_method, SUM(total) as total, COUNT(*) as count') + ->groupBy('payment_method') + ->get(); + } +} diff --git a/app/Models/Sale.php b/app/Models/Sale.php index b3560ad..57749da 100644 --- a/app/Models/Sale.php +++ b/app/Models/Sale.php @@ -17,6 +17,7 @@ class Sale extends Model { protected $fillable = [ 'user_id', + 'cash_register_id', 'invoice_number', 'subtotal', 'tax', @@ -40,4 +41,9 @@ public function details() { return $this->hasMany(SaleDetail::class); } + + public function cashRegister() + { + return $this->belongsTo(CashRegister::class); + } } diff --git a/app/Services/CashRegisterService.php b/app/Services/CashRegisterService.php new file mode 100644 index 0000000..48bf482 --- /dev/null +++ b/app/Services/CashRegisterService.php @@ -0,0 +1,95 @@ +where('status', 'open') + ->first(); + + if ($openRegister) { + throw new \Exception('Ya tienes una caja abierta. Debes cerrarla antes de abrir una nueva.'); + } + + return CashRegister::create([ + 'user_id' => $data['user_id'], + 'opened_at' => now(), + 'initial_cash' => $data['initial_cash'] ?? 0, + 'status' => 'open', + ]); + } + + /** + * Cerrar caja + */ + public function closeRegister(CashRegister $register, array $data) + { + if ($register->status === 'closed') { + throw new \Exception('Esta caja ya está cerrada.'); + } + + return DB::transaction(function () use ($register, $data) { + // Calcular ventas en efectivo + $cashSales = Sale::where('cash_register_id', $register->id) + ->where('payment_method', 'cash') + ->where('status', 'completed') + ->sum('total'); + + // Calcular efectivo esperado + $expectedCash = $register->initial_cash + $cashSales; + + // Calcular diferencia + $finalCash = $data['final_cash']; + $difference = $finalCash - $expectedCash; + + // Actualizar caja + $register->update([ + 'closed_at' => now(), + 'final_cash' => $finalCash, + 'expected_cash' => $expectedCash, + 'difference' => $difference, + 'total_sales' => Sale::where('cash_register_id', $register->id) + ->where('status', 'completed') + ->sum('total'), + 'sales_count' => Sale::where('cash_register_id', $register->id) + ->where('status', 'completed') + ->count(), + 'notes' => $data['notes'] ?? null, + 'status' => 'closed', + ]); + + return $register->fresh(['user', 'sales']); + }); + } + + /** + * Obtener resumen de caja actual + */ + public function getCurrentSummary(CashRegister $register) + { + $sales = Sale::where('cash_register_id', $register->id) + ->where('status', 'completed') + ->get(); + + return [ + 'register' => $register, + 'total_cash' => $sales->where('payment_method', 'cash')->sum('total'), + 'total_credit_card' => $sales->where('payment_method', 'credit_card')->sum('total'), + 'total_debit_card' => $sales->where('payment_method', 'debit_card')->sum('total'), + 'total_sales' => $sales->sum('total'), + 'sales_count' => $sales->count(), + 'expected_cash' => $register->initial_cash + $sales->where('payment_method', 'cash')->sum('total'), + ]; + } +} diff --git a/app/Services/SaleService.php b/app/Services/SaleService.php index 30e7c61..8e95a4c 100644 --- a/app/Services/SaleService.php +++ b/app/Services/SaleService.php @@ -17,6 +17,7 @@ public function createSale(array $data) // 1. Crear la venta principal $sale = Sale::create([ 'user_id' => $data['user_id'], + 'cash_register_id' => $data['cash_register_id'] ?? $this->getCurrentCashRegister($data['user_id']), 'invoice_number' => $data['invoice_number'] ?? $this->generateInvoiceNumber(), 'subtotal' => $data['subtotal'], 'tax' => $data['tax'], @@ -95,4 +96,13 @@ private function generateInvoiceNumber(): string return $prefix . $date . '-' . str_pad($sequential, 4, '0', STR_PAD_LEFT); } + + private function getCurrentCashRegister($userId) + { + $register = \App\Models\CashRegister::where('user_id', $userId) + ->where('status', 'open') + ->first(); + + return $register ? $register->id : null; + } } diff --git a/database/migrations/2025_12_31_132746_create_cash_registers_table.php b/database/migrations/2025_12_31_132746_create_cash_registers_table.php new file mode 100644 index 0000000..efd5b73 --- /dev/null +++ b/database/migrations/2025_12_31_132746_create_cash_registers_table.php @@ -0,0 +1,44 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('restrict'); + $table->timestamp('opened_at'); + $table->timestamp('closed_at')->nullable(); + $table->decimal('initial_cash', 10, 2)->default(0); + $table->decimal('final_cash', 10, 2)->nullable(); + $table->decimal('expected_cash', 10, 2)->nullable(); + $table->decimal('difference', 10, 2)->nullable(); + $table->decimal('total_sales', 10, 2)->default(0); + $table->integer('sales_count')->default(0); + $table->text('notes')->nullable(); + $table->enum('status', ['open', 'closed'])->default('open'); + $table->timestamps(); + + $table->index(['user_id', 'status']); + $table->index('opened_at'); + }); + + Schema::table('sales', function (Blueprint $table) { + $table->foreignId('cash_register_id')->nullable()->after('user_id')->constrained()->onDelete('restrict'); + }); + } + + public function down(): void + { + Schema::table('sales', function (Blueprint $table) { + $table->dropForeign(['cash_register_id']); + $table->dropColumn('cash_register_id'); + }); + + Schema::dropIfExists('cash_registers'); + } +}; diff --git a/routes/api.php b/routes/api.php index 5059e69..33c1b5b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@