From c68a16763ca0aa7392b4fafcc8ea5b249a4d36c6 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Wed, 4 Feb 2026 14:26:29 -0600 Subject: [PATCH] feat: agregar funcionalidad para subir archivos de factura y almacenar UUID del CFDI --- Docker/nginx/nginx.conf | 2 +- .../App/InvoiceRequestController.php | 86 +++++++++++++++++++ .../App/InvoiceRequestUploadRequest.php | 53 ++++++++++++ app/Models/InvoiceRequest.php | 35 ++++++++ ...nvoice_files_to_invoice_requests_table.php | 30 +++++++ docker-compose.yml | 1 + routes/api.php | 4 +- 7 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 app/Http/Requests/App/InvoiceRequestUploadRequest.php create mode 100644 database/migrations/2026_02_04_105223_add_invoice_files_to_invoice_requests_table.php diff --git a/Docker/nginx/nginx.conf b/Docker/nginx/nginx.conf index 6419d9d..5530aed 100644 --- a/Docker/nginx/nginx.conf +++ b/Docker/nginx/nginx.conf @@ -45,7 +45,7 @@ server { # Handle storage files (Laravel storage link) location /storage { - alias /var/www/pdv.backend/storage/app; + alias /var/www/pdv.backend/storage/app/public; try_files $uri =404; } diff --git a/app/Http/Controllers/App/InvoiceRequestController.php b/app/Http/Controllers/App/InvoiceRequestController.php index 24a1685..c22c388 100644 --- a/app/Http/Controllers/App/InvoiceRequestController.php +++ b/app/Http/Controllers/App/InvoiceRequestController.php @@ -5,8 +5,10 @@ use App\Http\Controllers\Controller; use App\Http\Requests\App\InvoiceRequestProcessRequest; use App\Http\Requests\App\InvoiceRequestRejectRequest; +use App\Http\Requests\App\InvoiceRequestUploadRequest; use App\Models\InvoiceRequest; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; use Notsoweb\ApiResponse\Enums\ApiResponse; /** @@ -195,4 +197,88 @@ public function stats() '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 + ]); + } } diff --git a/app/Http/Requests/App/InvoiceRequestUploadRequest.php b/app/Http/Requests/App/InvoiceRequestUploadRequest.php new file mode 100644 index 0000000..19b53da --- /dev/null +++ b/app/Http/Requests/App/InvoiceRequestUploadRequest.php @@ -0,0 +1,53 @@ + + * + * @version 1.0.0 + */ +class InvoiceRequestUploadRequest extends FormRequest +{ + /** + * Determinar si el usuario está autorizado para realizar esta solicitud + */ + public function authorize(): bool + { + return true; // Autorización manejada por middleware + } + + /** + * Obtener las reglas de validación que se aplican a la solicitud + */ + public function rules(): array + { + return [ + 'invoice_xml' => ['nullable', 'file', 'mimes:xml', 'max:2048'], // Max 2MB + 'invoice_pdf' => ['required', 'file', 'mimes:pdf', 'max:5120'], // Max 5MB + 'cfdi_uuid' => ['required', 'string', 'size:36', 'regex:/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i'], + ]; + } + + /** + * Mensajes personalizados de validación + */ + public function messages(): array + { + return [ + 'invoice_xml.mimes' => 'El archivo debe ser un XML válido', + 'invoice_xml.max' => 'El archivo XML no puede ser mayor a 2MB', + 'invoice_pdf.required' => 'El archivo PDF de la factura es obligatorio', + 'invoice_pdf.mimes' => 'El archivo debe ser un PDF válido', + 'invoice_pdf.max' => 'El archivo PDF no puede ser mayor a 5MB', + 'cfdi_uuid.required' => 'El UUID del CFDI es obligatorio', + 'cfdi_uuid.size' => 'El UUID del CFDI debe tener 36 caracteres', + 'cfdi_uuid.regex' => 'El formato del UUID del CFDI no es válido', + ]; + } +} diff --git a/app/Models/InvoiceRequest.php b/app/Models/InvoiceRequest.php index a24a563..89c7675 100644 --- a/app/Models/InvoiceRequest.php +++ b/app/Models/InvoiceRequest.php @@ -5,6 +5,8 @@ use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; /** * Descripción @@ -23,6 +25,9 @@ class InvoiceRequest extends Model 'processed_at', 'processed_by', 'notes', + 'invoice_xml_path', + 'invoice_pdf_path', + 'cfdi_uuid', ]; protected $casts = [ @@ -30,6 +35,12 @@ class InvoiceRequest extends Model 'processed_at' => 'datetime', ]; + + /** + * Atributos adicionales para serializar + */ + protected $appends = ['invoice_xml_url', 'invoice_pdf_url']; + const STATUS_PENDING = 'pending'; const STATUS_PROCESSED = 'processed'; const STATUS_REJECTED = 'rejected'; @@ -74,4 +85,28 @@ public function markAsRejected(int $userId, ?string $notes = null): bool 'notes' => $notes ?? $this->notes, ]); } + + /** + * Obtener la URL completa del archivo XML + */ + public function getInvoiceXmlUrlAttribute() + { + if (!$this->invoice_xml_path) { + return null; + } + + return Storage::disk('public')->url($this->invoice_xml_path); + } + + /** + * Obtener la URL completa del archivo PDF + */ + public function getInvoicePdfUrlAttribute() + { + if (!$this->invoice_pdf_path) { + return null; + } + + return Storage::disk('public')->url($this->invoice_pdf_path); + } } diff --git a/database/migrations/2026_02_04_105223_add_invoice_files_to_invoice_requests_table.php b/database/migrations/2026_02_04_105223_add_invoice_files_to_invoice_requests_table.php new file mode 100644 index 0000000..099fde2 --- /dev/null +++ b/database/migrations/2026_02_04_105223_add_invoice_files_to_invoice_requests_table.php @@ -0,0 +1,30 @@ +string('invoice_xml_path')->nullable()->after('notes'); + $table->string('invoice_pdf_path')->nullable()->after('invoice_xml_path'); + $table->string('cfdi_uuid')->nullable()->after('invoice_pdf_path'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('invoice_requests', function (Blueprint $table) { + $table->dropColumn(['invoice_xml_path', 'invoice_pdf_path', 'cfdi_uuid']); + }); + } +}; diff --git a/docker-compose.yml b/docker-compose.yml index 2540720..6778438 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: - "${NGINX_PORT}:80" volumes: - ./public:/var/www/pdv.backend/public + - ./storage:/var/www/pdv.backend/storage - ./Docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf networks: - pdv-network diff --git a/routes/api.php b/routes/api.php index 2eaf67b..afd5033 100644 --- a/routes/api.php +++ b/routes/api.php @@ -38,7 +38,7 @@ Route::post('inventario/import', [InventoryController::class, 'import']); Route::get('inventario/template/download', [InventoryController::class, 'downloadTemplate']); - // Números de serie + // NÚMEROS DE SERIE DE INVENTARIO Route::resource('inventario.serials', InventorySerialController::class); Route::get('serials/search', [InventorySerialController::class, 'search']); @@ -101,12 +101,14 @@ Route::patch('/{tier}/toggle-active', [ClientTierController::class, 'toggleActive']); }); + // SOLICITUDES DE FACTURACIÓN Route::prefix('invoice-requests')->group(function () { Route::get('/', [InvoiceRequestController::class, 'index']); Route::get('/stats', [InvoiceRequestController::class, 'stats']); Route::get('/{id}', [InvoiceRequestController::class, 'show']); Route::put('/{id}/process', [InvoiceRequestController::class, 'process']); Route::put('/{id}/reject', [InvoiceRequestController::class, 'reject']); + Route::post('/{id}/upload', [InvoiceRequestController::class, 'uploadInvoiceFile']); }); });