with([ 'client', 'details.inventory.category', 'details.serials', 'user:id,name,email' ]) ->first(); if (!$sale) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Venta no encontrada' ]); } // Si ya tiene datos de facturación if ($sale->client_id) { return ApiResponse::NO_CONTENT->response([ 'message' => 'Esta venta ya tiene datos de facturación registrados', 'client' => $sale->client, 'sale' => $this->formatSaleData($sale) ]); } return ApiResponse::OK->response([ 'sale' => $this->formatSaleData($sale) ]); } /** * Guarda los datos fiscales del cliente para la venta. */ public function store(Request $request, string $invoiceNumber) { $sale = Sale::where('invoice_number', $invoiceNumber) ->with('details.serials') ->first(); if (!$sale) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Venta no encontrada' ]); } if ($sale->client_id) { return ApiResponse::NO_CONTENT->response([ 'message' => 'Esta venta ya tiene datos de facturación registrados' ]); } // Verificar que la venta esté completada if ($sale->status !== 'completed') { return ApiResponse::BAD_REQUEST->response([ 'message' => 'Solo se pueden facturar ventas completadas' ]); } $validated = $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|max:255', 'phone' => 'nullable|string|max:20', 'address' => 'nullable|string|max:500', 'rfc' => 'required|string|size:13|regex:/^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/i', 'razon_social' => 'required|string|max:255', 'regimen_fiscal' => 'required|string|max:100', 'cp_fiscal' => 'required|string|size:5|regex:/^\d{5}$/', 'uso_cfdi' => 'required|string|max:10', ], [ 'rfc.regex' => 'El RFC no tiene un formato válido', 'rfc.size' => 'El RFC debe tener 13 caracteres', 'cp_fiscal.regex' => 'El código postal debe ser de 5 dígitos', 'cp_fiscal.size' => 'El código postal debe ser de 5 dígitos', 'name.required' => 'El nombre es obligatorio', 'email.required' => 'El correo electrónico es obligatorio', 'email.email' => 'El correo electrónico debe ser válido', 'razon_social.required' => 'La razón social es obligatoria', 'regimen_fiscal.required' => 'El régimen fiscal es obligatorio', 'uso_cfdi.required' => 'El uso de CFDI es obligatorio', ]); // Buscar si ya existe un cliente con ese RFC $client = Client::where('rfc', strtoupper($validated['rfc']))->first(); if ($client) { // Actualizar datos del cliente existente $client->update([ 'name' => $validated['name'], 'email' => $validated['email'], 'phone' => $validated['phone'] ?? $client->phone, 'address' => $validated['address'] ?? $client->address, 'razon_social' => $validated['razon_social'], 'regimen_fiscal' => $validated['regimen_fiscal'], 'cp_fiscal' => $validated['cp_fiscal'], 'uso_cfdi' => $validated['uso_cfdi'], ]); } else { // Crear nuevo cliente $client = Client::create([ 'name' => $validated['name'], 'email' => $validated['email'], 'phone' => $validated['phone'], 'address' => $validated['address'], 'rfc' => strtoupper($validated['rfc']), 'razon_social' => $validated['razon_social'], 'regimen_fiscal' => $validated['regimen_fiscal'], 'cp_fiscal' => $validated['cp_fiscal'], 'uso_cfdi' => $validated['uso_cfdi'], ]); } // Asociar cliente a la venta $sale->update(['client_id' => $client->id]); // Recargar relaciones $sale->load([ 'client', 'details.inventory.category', 'details.serials', 'user:id,name,email' ]); return ApiResponse::OK->response([ 'message' => 'Datos de facturación guardados correctamente', 'client' => $client, 'sale' => $this->formatSaleData($sale) ]); } /** * Formatear datos de la venta incluyendo números de serie */ private function formatSaleData(Sale $sale): array { return [ 'id' => $sale->id, 'invoice_number' => $sale->invoice_number, 'total' => $sale->total, 'subtotal' => $sale->subtotal, 'tax' => $sale->tax, 'payment_method' => $sale->payment_method, 'status' => $sale->status, 'created_at' => $sale->created_at, 'user' => $sale->user ? [ 'id' => $sale->user->id, 'name' => $sale->user->name, 'email' => $sale->user->email, ] : null, 'items' => $sale->details->map(function ($detail) { return [ 'id' => $detail->id, 'product_name' => $detail->product_name, 'quantity' => $detail->quantity, 'unit_price' => $detail->unit_price, 'subtotal' => $detail->subtotal, 'category' => $detail->inventory->category->name ?? null, 'sku' => $detail->inventory->sku ?? null, // Números de serie vendidos 'serial_numbers' => $detail->serials->map(function ($serial) { return [ 'serial_number' => $serial->serial_number, 'status' => $serial->status, ]; })->toArray(), ]; })->toArray(), ]; } }