has('status') && $request->status !== '') { $query->where('status', $request->status); } // Filtro por rango de fechas if ($request->has('date_from')) { $query->whereDate('requested_at', '>=', $request->date_from); } if ($request->has('date_to')) { $query->whereDate('requested_at', '<=', $request->date_to); } // Búsqueda por folio de venta if ($request->has('invoice_number') && $request->invoice_number !== '') { $query->whereHas('sale', function ($q) use ($request) { $q->where('invoice_number', 'like', '%' . $request->invoice_number . '%'); }); } // Búsqueda por cliente if ($request->has('client_search') && $request->client_search !== '') { $query->whereHas('client', function ($q) use ($request) { $q->where('name', 'like', '%' . $request->client_search . '%') ->orWhere('rfc', 'like', '%' . $request->client_search . '%') ->orWhere('email', 'like', '%' . $request->client_search . '%'); }); } // Ordenamiento $sortBy = $request->get('sort_by', 'requested_at'); $sortOrder = $request->get('sort_order', 'desc'); $query->orderBy($sortBy, $sortOrder); // Paginación $perPage = $request->get('per_page', 15); $invoiceRequests = $query->paginate($perPage); return ApiResponse::OK->response([ 'invoice_requests' => $invoiceRequests ]); } /** * Mostrar detalles de una solicitud específica */ public function show($id) { $invoiceRequest = InvoiceRequest::with([ 'sale.user:id,name,email', 'sale.details:id,sale_id,inventory_id,product_name,quantity,unit_price,subtotal,discount_percentage,discount_amount', 'sale.details:inventory_id.category_id,name', 'sale.details.inventory:id,category_id,name,sku', 'sale.details.inventory.category:id,name', 'sale.details.serials:id,inventory_id,sale_detail_id,serial_number,status', 'client', 'processedBy:id,name,email' ])->find($id); if (!$invoiceRequest) { return ApiResponse::NOT_FOUND->response([ 'message' => 'Solicitud de factura no encontrada' ]); } return ApiResponse::OK->response([ 'invoice_request' => $invoiceRequest ]); } /** * Marcar una solicitud como procesada * */ public function process(InvoiceRequestProcessRequest $request, $id) { $invoiceRequest = InvoiceRequest::find($id); if (!$invoiceRequest) { return ApiResponse::NOT_FOUND->response([ 'message' => 'Solicitud de factura no encontrada' ]); } if ($invoiceRequest->status !== InvoiceRequest::STATUS_PENDING) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'Solo se pueden procesar solicitudes pendientes', 'current_status' => $invoiceRequest->status ]); } $invoiceRequest->markAsProcessed( $request->user()->id, $request->notes ); $invoiceRequest->load([ 'sale:id,invoice_number,total', 'client:id,name,rfc,email', 'processedBy:id,name' ]); return ApiResponse::OK->response([ 'message' => 'Solicitud marcada como procesada correctamente', 'invoice_request' => $invoiceRequest ]); } /** * Rechazar una solicitud * */ public function reject(InvoiceRequestRejectRequest $request, $id) { $invoiceRequest = InvoiceRequest::find($id); if (!$invoiceRequest) { return ApiResponse::NOT_FOUND->response([ 'message' => 'Solicitud de factura no encontrada' ]); } if ($invoiceRequest->status !== InvoiceRequest::STATUS_PENDING) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'Solo se pueden rechazar solicitudes pendientes', 'current_status' => $invoiceRequest->status ]); } $invoiceRequest->markAsRejected( $request->user()->id, $request->notes ); $invoiceRequest->load([ 'sale:id,invoice_number,total', 'client:id,name,rfc,email', 'processedBy:id,name' ]); return ApiResponse::OK->response([ 'message' => 'Solicitud rechazada correctamente', 'invoice_request' => $invoiceRequest ]); } /** * Obtener estadísticas de solicitudes de factura */ public function stats() { $stats = [ 'pending' => InvoiceRequest::where('status', InvoiceRequest::STATUS_PENDING)->count(), 'processed' => InvoiceRequest::where('status', InvoiceRequest::STATUS_PROCESSED)->count(), 'rejected' => InvoiceRequest::where('status', InvoiceRequest::STATUS_REJECTED)->count(), 'total' => InvoiceRequest::count(), 'today_pending' => InvoiceRequest::where('status', InvoiceRequest::STATUS_PENDING) ->whereDate('requested_at', today()) ->count(), 'this_month' => InvoiceRequest::whereMonth('requested_at', now()->month) ->whereYear('requested_at', now()->year) ->count(), ]; return ApiResponse::OK->response([ 'stats' => $stats ]); } /** * Subir archivos de factura (XML y PDF) y guardar UUID del CFDI */ public function uploadInvoiceFile(InvoiceRequestUploadRequest $request, $id) { $invoiceRequest = InvoiceRequest::find($id); if (!$invoiceRequest) { return ApiResponse::NOT_FOUND->response([ 'message' => 'Solicitud de factura no encontrada' ]); } // Validar que la solicitud esté pendiente o procesada (permitir actualizar facturas) if (!in_array($invoiceRequest->status, [InvoiceRequest::STATUS_PENDING, InvoiceRequest::STATUS_PROCESSED])) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'No se pueden subir archivos a solicitudes rechazadas', 'current_status' => $invoiceRequest->status ]); } // Generar timestamp para nombres de archivo $timestamp = now()->format('YmdHis'); $storagePath = 'invoices/' . now()->format('Y/m'); $updateData = [ 'cfdi_uuid' => $request->cfdi_uuid, ]; // Procesar XML si se envió if ($request->hasFile('invoice_xml')) { // Eliminar XML anterior si existe if ($invoiceRequest->invoice_xml_path) { Storage::disk('public')->delete($invoiceRequest->invoice_xml_path); } $xmlFileName = "invoice-{$id}-{$timestamp}.xml"; $xmlPath = $request->file('invoice_xml')->storeAs( $storagePath, $xmlFileName, 'public' ); $updateData['invoice_xml_path'] = $xmlPath; } // Procesar PDF (siempre requerido) if ($request->hasFile('invoice_pdf')) { // Eliminar PDF anterior si existe if ($invoiceRequest->invoice_pdf_path) { Storage::disk('public')->delete($invoiceRequest->invoice_pdf_path); } $pdfFileName = "invoice-{$id}-{$timestamp}.pdf"; $pdfPath = $request->file('invoice_pdf')->storeAs( $storagePath, $pdfFileName, 'public' ); $updateData['invoice_pdf_path'] = $pdfPath; } // Actualizar registro $invoiceRequest->update($updateData); $invoiceRequest->load([ 'sale:id,invoice_number', 'client:id,name,rfc', ]); $files = []; if (isset($xmlPath)) { $files['xml_url'] = Storage::url($xmlPath); } if (isset($pdfPath)) { $files['pdf_url'] = Storage::url($pdfPath); } return ApiResponse::OK->response([ 'message' => 'Archivos de factura subidos correctamente', 'invoice_request' => $invoiceRequest, 'files' => $files ]); } }