$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'], 'total' => $data['total'], 'cash_received' => $cashReceived, 'change' => $change, 'payment_method' => $data['payment_method'], 'status' => $data['status'] ?? 'completed', ]); // 2. Crear los detalles de la venta y asignar seriales foreach ($data['items'] as $item) { // Crear detalle de venta $saleDetail = SaleDetail::create([ 'sale_id' => $sale->id, 'catalog_item_id' => $item['catalog_item_id'], 'product_name' => $item['product_name'], 'quantity' => $item['quantity'], 'unit_price' => $item['unit_price'], 'subtotal' => $item['subtotal'], 'serial_numbers' => $item['serial_numbers'] ?? null, // Si vienen del frontend ]); // Obtener el inventario $catalogItem = CatalogItem::find($item['catalog_item']); if ($catalogItem && $catalogItem->is_stockable) { // Si se proporcionaron números de serie específicos if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) { foreach ($item['serial_numbers'] as $serialNumber) { $serial = InventorySerial::where('catalog_item_id', $catalogItem->id) ->where('serial_number', $serialNumber) ->where('status', 'disponible') ->first(); if ($serial) { $serial->markAsSold($saleDetail->id); } else { throw new \Exception("Serial {$serialNumber} no disponible"); } } } else { // Asignar automáticamente los primeros N seriales disponibles $serials = InventorySerial::where('catalog_item_id', $catalogItem->id) ->where('status', 'disponible') ->limit($item['quantity']) ->get(); if ($serials->count() < $item['quantity']) { throw new \Exception("Stock insuficiente de seriales para {$item['product_name']}"); } foreach ($serials as $serial) { $serial->markAsSold($saleDetail->id); } } // Sincronizar el stock $catalogItem->syncStock(); } } // 3. Retornar la venta con sus relaciones cargadas return $sale->load(['details.catalogItem', 'details.serials', 'user']); }); } /** * Cancelar una venta y restaurar el stock * */ public function cancelSale(Sale $sale) { return DB::transaction(function () use ($sale) { // Verificar que la venta esté completada if ($sale->status !== 'completed') { throw new \Exception('Solo se pueden cancelar ventas completadas.'); } // Restaurar seriales a disponible foreach ($sale->details as $detail) { $serials = InventorySerial::where('sale_detail_id', $detail->id)->get(); foreach ($serials as $serial) { $serial->markAsAvailable(); } // Sincronizar stock $detail->catalogItem->syncStock(); } // Marcar venta como cancelada $sale->update(['status' => 'cancelled']); return $sale->fresh(['details.catalogItem', 'details.serials', 'user']); }); } /** * Generar número de factura único * Formato: INV-YYYYMMDD-0001 */ private function generateInvoiceNumber(): string { $prefix = 'INV-'; $date = now()->format('Ymd'); // Obtener la última venta del día $lastSale = Sale::whereDate('created_at', today()) ->orderBy('id', 'desc') ->first(); // Incrementar secuencial $sequential = $lastSale ? (intval(substr($lastSale->invoice_number, -4)) + 1) : 1; return $prefix . $date . '-' . str_pad($sequential, 4, '0', STR_PAD_LEFT); } private function getCurrentCashRegister($userId) { $register = CashRegister::where('user_id', $userId) ->where('status', 'open') ->first(); return $register ? $register->id : null; } }