feat: agregar funcionalidad para subir archivos de factura y almacenar UUID del CFDI
This commit is contained in:
parent
f2d2fd5aaf
commit
c68a16763c
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
53
app/Http/Requests/App/InvoiceRequestUploadRequest.php
Normal file
53
app/Http/Requests/App/InvoiceRequestUploadRequest.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php namespace App\Http\Requests\App;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('invoice_requests', function (Blueprint $table) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user