with([ 'client', 'details.inventory.category', 'details.serials', 'user:id,name,email', 'invoiceRequests' => function ($query) { $query->orderBy('requested_at', 'desc'); }, ])->first(); if (!$sale) { return ApiResponse::NOT_FOUND->response([ 'message' => 'Venta no encontrada' ]); } // Verificar si ya tiene solicitud pendiente o procesada $existingRequest = $sale->invoiceRequests ->whereIn('status', [InvoiceRequest::STATUS_PENDING, InvoiceRequest::STATUS_PROCESSED]) ->first(); if ($existingRequest) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'Esta venta ya tiene una solicitud de facturación ' . ($existingRequest->status === InvoiceRequest::STATUS_PENDING ? 'pendiente' : 'procesada'), 'invoice_request' => $existingRequest, ]); } // Buscar cliente existente si la venta ya tiene cliente asociado $existingClient = null; if ($sale->client_id) { $existingClient = $sale->client; } return ApiResponse::OK->response([ 'sale' => $this->formatSaleData($sale), 'client' => $sale->client, 'existing_client' => $existingClient, 'invoice_requests' => $sale->invoiceRequests, ]); } /** * Guarda los datos fiscales del cliente para la venta. */ public function store(InvoiceStoreRequest $request, string $invoiceNumber) { $sale = Sale::where('invoice_number', $invoiceNumber) ->with('details.serials') ->first(); if (!$sale) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Venta no encontrada' ]); } $existingRequest = InvoiceRequest::where('sale_id', $sale->id) ->whereIn('status', [InvoiceRequest::STATUS_PENDING, InvoiceRequest::STATUS_PROCESSED]) ->first(); if ($existingRequest) { return ApiResponse::NO_CONTENT->response([ 'message' => 'Esta venta ya tiene una solicitud de facturación ' . ($existingRequest->status === InvoiceRequest::STATUS_PENDING ? 'pendiente' : 'procesada') ]); } // Verificar que la venta esté completada if ($sale->status !== 'completed') { return ApiResponse::BAD_REQUEST->response([ 'message' => 'Solo se pueden facturar ventas completadas' ]); } // Buscar si ya existe un cliente con ese RFC $client = Client::where('rfc', strtoupper($request->rfc))->first(); if ($client) { // Solo actualizar campos que el usuario proporciona explícitamente $updateData = []; // Actualizar nombre solo si es diferente y fue proporcionado if ($request->filled('name') && $request->name !== $client->name) { $updateData['name'] = $request->name; } // Actualizar email solo si es diferente y fue proporcionado if ($request->filled('email') && $request->email !== $client->email) { $updateData['email'] = $request->email; } // Actualizar teléfono si fue proporcionado if ($request->filled('phone')) { $updateData['phone'] = $request->phone; } // Actualizar dirección si fue proporcionada if ($request->filled('address')) { $updateData['address'] = $request->address; } // Actualizar datos fiscales siempre (son obligatorios en el request) $updateData['razon_social'] = $request->razon_social; $updateData['regimen_fiscal'] = $request->regimen_fiscal; $updateData['cp_fiscal'] = $request->cp_fiscal; $updateData['uso_cfdi'] = $request->uso_cfdi; // Solo actualizar si hay cambios if (!empty($updateData)) { $client->update($updateData); } } else { // Crear nuevo cliente $client = Client::create([ 'name' => $request->name, 'client_number' => strtoupper($request->rfc), 'email' => $request->email, 'phone' => $request->phone, 'address' => $request->address, 'rfc' => strtoupper($request->rfc), 'razon_social' => $request->razon_social, 'regimen_fiscal' => $request->regimen_fiscal, 'cp_fiscal' => $request->cp_fiscal, 'uso_cfdi' => $request->uso_cfdi, ]); } // Asociar cliente a la venta solo si no está ya asociado if ($sale->client_id !== $client->id) { $sale->update(['client_id' => $client->id]); } // Crear solicitud de facturación $invoiceRequest = InvoiceRequest::create([ 'sale_id' => $sale->id, 'client_id' => $client->id, 'status' => InvoiceRequest::STATUS_PENDING, 'requested_at' => now(), ]); return ApiResponse::OK->response([ 'message' => 'Solicitud de facturación guardada correctamente', 'client' => $client->fresh(), 'sale' => $this->formatSaleData($sale->fresh('details.serials')), 'invoice_request' => $invoiceRequest, ]); } /** * Verificar si existe un cliente con el RFC proporcionado */ public function checkRfc(Request $request) { $request->validate([ 'rfc' => ['required', 'string', 'min:12', 'max:13'], ]); $rfc = $request->input('rfc'); $client = Client::where('rfc', strtoupper($rfc))->first(); if ($client) { return ApiResponse::OK->response([ 'exists' => true, 'client' => [ 'name' => $client->name, 'email' => $client->email, 'phone' => $client->phone, 'address' => $client->address, 'rfc' => $client->rfc, 'razon_social' => $client->razon_social, 'regimen_fiscal' => $client->regimen_fiscal, 'cp_fiscal' => $client->cp_fiscal, 'uso_cfdi' => $client->uso_cfdi, ] ]); } return ApiResponse::OK->response([ 'exists' => false ]); } /** * 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(), ]; } }