ADD: Corte de caja

This commit is contained in:
Juan Felipe Zapata Moreno 2025-12-31 13:45:42 -06:00
parent 569fbd09d7
commit 599bb68ce6
7 changed files with 362 additions and 0 deletions

View File

@ -0,0 +1,138 @@
<?php
namespace App\Http\Controllers\App;
use App\Http\Controllers\Controller;
use App\Models\CashRegister;
use App\Services\CashRegisterService;
use Illuminate\Http\Request;
use Notsoweb\ApiResponse\Enums\ApiResponse;
class CashRegisterController extends Controller
{
public function __construct(
protected CashRegisterService $cashRegisterService
) {}
/**
* Listar cortes de caja
*/
public function index(Request $request)
{
$query = CashRegister::with('user')->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()
]);
}
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CashRegister extends Model
{
protected $fillable = [
'user_id',
'opened_at',
'closed_at',
'initial_cash',
'final_cash',
'expected_cash',
'difference',
'total_sales',
'sales_count',
'notes',
'status',
];
protected $casts = [
'opened_at' => '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();
}
}

View File

@ -17,6 +17,7 @@ class Sale extends Model
{ {
protected $fillable = [ protected $fillable = [
'user_id', 'user_id',
'cash_register_id',
'invoice_number', 'invoice_number',
'subtotal', 'subtotal',
'tax', 'tax',
@ -40,4 +41,9 @@ public function details()
{ {
return $this->hasMany(SaleDetail::class); return $this->hasMany(SaleDetail::class);
} }
public function cashRegister()
{
return $this->belongsTo(CashRegister::class);
}
} }

View File

@ -0,0 +1,95 @@
<?php
namespace App\Services;
use App\Models\CashRegister;
use App\Models\Sale;
use Illuminate\Support\Facades\DB;
class CashRegisterService
{
/**
* Abrir caja
*/
public function openRegister(array $data)
{
// Verificar que el usuario no tenga una caja abierta
$openRegister = CashRegister::where('user_id', $data['user_id'])
->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'),
];
}
}

View File

@ -17,6 +17,7 @@ public function createSale(array $data)
// 1. Crear la venta principal // 1. Crear la venta principal
$sale = Sale::create([ $sale = Sale::create([
'user_id' => $data['user_id'], 'user_id' => $data['user_id'],
'cash_register_id' => $data['cash_register_id'] ?? $this->getCurrentCashRegister($data['user_id']),
'invoice_number' => $data['invoice_number'] ?? $this->generateInvoiceNumber(), 'invoice_number' => $data['invoice_number'] ?? $this->generateInvoiceNumber(),
'subtotal' => $data['subtotal'], 'subtotal' => $data['subtotal'],
'tax' => $data['tax'], 'tax' => $data['tax'],
@ -95,4 +96,13 @@ private function generateInvoiceNumber(): string
return $prefix . $date . '-' . str_pad($sequential, 4, '0', STR_PAD_LEFT); 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;
}
} }

View File

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('cash_registers', function (Blueprint $table) {
$table->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');
}
};

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Http\Controllers\App\CashRegisterController;
use App\Http\Controllers\App\CategoryController; use App\Http\Controllers\App\CategoryController;
use App\Http\Controllers\App\InventoryController; use App\Http\Controllers\App\InventoryController;
use App\Http\Controllers\App\PriceController; use App\Http\Controllers\App\PriceController;
@ -37,6 +38,12 @@
Route::resource('/sales', SaleController::class); Route::resource('/sales', SaleController::class);
Route::put('/sales/{sale}/cancel', [SaleController::class, 'cancel']); Route::put('/sales/{sale}/cancel', [SaleController::class, 'cancel']);
// Rutas de caja
Route::get('/', [CashRegisterController::class, 'index']);
Route::get('/current', [CashRegisterController::class, 'current']);
Route::get('/{register}', [CashRegisterController::class, 'show']);
Route::post('/open', [CashRegisterController::class, 'open']);
Route::put('/{register}/close', [CashRegisterController::class, 'close']);
}); });