diff --git a/app/Http/Controllers/Repuve/InscriptionController.php b/app/Http/Controllers/Repuve/InscriptionController.php new file mode 100644 index 0000000..6a22e52 --- /dev/null +++ b/app/Http/Controllers/Repuve/InscriptionController.php @@ -0,0 +1,568 @@ +input('folio'); + $tagId = $request->input('tag_id'); + + // Buscar Tag y validar que NO tenga vehículo asignado + $tag = Tag::findOrFail($tagId); + + if ($tag->vehicle_id) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'El tag ya está asignado a un vehículo. Use actualizar en su lugar.', + 'tag_id' => $tagId, + 'vehicle_id' => $tag->vehicle_id, + ]); + } + + // Validar que el folio del tag coincida + if ($tag->folio !== $folio) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'El folio no coincide con el tag RFID proporcionado', + 'folio_request' => $folio, + 'folio_tag' => $tag->folio, + ]); + } + + // Verificar robo (API Repuve Nacional) + $isStolen = $this->checkIfStolen($folio); + + if ($isStolen) { + return ApiResponse::FORBIDDEN->response([ + 'folio' => $folio, + 'tag_id' => $tagId, + 'stolen' => true, + 'message' => 'El vehículo reporta robo. No se puede continuar con la inscripción.', + ]); + } + + // Iniciar transacción + DB::beginTransaction(); + + // Obtener 37 datos de API Estatal + $vehicleData = $this->getVehicle(); + $ownerData = $this->getOwner(); + + // Crear propietario + $owner = Owner::updateOrCreate( + ['rfc' => $ownerData['rfc']], + [ + 'name' => $ownerData['name'], + 'paternal' => $ownerData['paternal'], + 'maternal' => $ownerData['maternal'], + 'curp' => $ownerData['curp'], + 'address' => $ownerData['address'], + ] + ); + + // Crear vehículo + $vehicle = Vehicle::create([ + 'anio_placa' => $vehicleData['ANIO_PLACA'], + 'placa' => $vehicleData['PLACA'], + 'numero_serie' => $vehicleData['NO_SERIE'], + 'rfc' => $vehicleData['RFC'], + 'folio' => $folio, // Folio del request + 'vigencia' => $vehicleData['VIGENCIA'], + 'fecha_impresion' => $vehicleData['FECHA_IMPRESION'], + 'qr_hash' => $vehicleData['QR_HASH'], + 'valido' => $vehicleData['VALIDO'], + 'nombre' => $vehicleData['NOMBRE'], + 'nombre2' => $vehicleData['NOMBRE2'], + 'municipio' => $vehicleData['MUNICIPIO'], + 'localidad' => $vehicleData['LOCALIDAD'], + 'calle' => $vehicleData['CALLE'], + 'calle2' => $vehicleData['CALLE2'], + 'tipo' => $vehicleData['TIPO'], + 'tipo_servicio' => $vehicleData['TIPO_SERVICIO'], + 'marca' => $vehicleData['MARCA'], + 'linea' => $vehicleData['LINEA'], + 'sublinea' => $vehicleData['SUBLINEA'], + 'modelo' => $vehicleData['MODELO'], + 'numero_motor' => $vehicleData['NUMERO_MOTOR'], + 'descripcion_origen' => $vehicleData['DESCRIPCION_ORIGEN'], + 'color' => $vehicleData['COLOR'], + 'codigo_postal' => $vehicleData['CODIGO_POSTAL'], + 'serie_folio' => $vehicleData['SERIE_FOLIO'], + 'sfolio' => $vehicleData['SFOLIO'], + 'nrpv' => $vehicleData['NUMERO_SERIE'], + 'owner_id' => $owner->id, + ]); + + // Asignar Tag al vehículo + $tag->update([ + 'vehicle_id' => $vehicle->id, + 'folio' => $folio, + ]); + + // Crear registro + $record = Record::create([ + 'folio' => $folio, + 'vehicle_id' => $vehicle->id, + 'user_id' => Auth::id(), + ]); + + // Procesar archivos + $uploadedFiles = []; + if ($request->hasFile('files')) { + $files = $request->file('files'); + $fileNames = $request->input('names', []); + + foreach ($files as $index => $file) { + $fileName = uniqid() . '_' . time() . '_' . $file->getClientOriginalName(); + $path = $file->storeAs('records', $fileName, 'public'); + $md5 = md5_file($file->getRealPath()); + + $fileRecord = File::create([ + 'name' => $fileNames[$index] ?? "Archivo " . ($index + 1), + 'path' => $path, + 'md5' => $md5, + 'record_id' => $record->id, + ]); + + $uploadedFiles[] = [ + 'id' => $fileRecord->id, + 'name' => $fileRecord->name, + 'path' => $fileRecord->path, + 'url' => $fileRecord->url, + ]; + } + } + + // Enviar a API Repuve Nacional + $apiResponse = $this->sendToRepuveNacional($folio, $tagId, $vehicleData); + + // Procesar respuesta + if (isset($apiResponse['has_error']) && $apiResponse['has_error']) { + // Si hay error, buscar el error en la BD + $error = Error::where('code', $apiResponse['error_code'])->first(); + + if (!$error) { + DB::rollBack(); + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Código de error no encontrado en el catálogo', + 'error_code' => $apiResponse['error_code'], + 'error_message' => $apiResponse['error_message'], + ]); + } + + // Guardar error en Record + $record->update([ + 'error_id' => $error->id, + 'api_response' => $apiResponse, + 'error_occurred_at' => now(), + ]); + + DB::commit(); + + // Retornar con error + return ApiResponse::OK->response([ + 'message' => 'Vehículo inscrito con error. Corrija los datos usando la función de actualización.', + 'has_error' => true, + 'can_update' => true, + 'record_id' => $record->id, + 'error' => [ + 'code' => $error->code, + 'description' => $error->description, + 'occurred_at' => $record->error_occurred_at->toDateTimeString(), + ], + 'record' => [ + 'id' => $record->id, + 'folio' => $record->folio, + ], + 'vehicle' => $vehicleData, + 'owner' => $ownerData, + 'files' => $uploadedFiles, + 'total_files' => count($uploadedFiles), + ]); + } + + // Si NO hay error, guardar respuesta exitosa + $record->update([ + 'error_id' => null, + 'api_response' => $apiResponse, + 'error_occurred_at' => null, + ]); + + DB::commit(); + + // Responder con éxito + return ApiResponse::OK->response([ + 'message' => 'Vehículo inscrito exitosamente', + 'has_error' => false, + 'stolen' => false, + 'record' => [ + 'id' => $record->id, + 'folio' => $record->folio, + 'vehicle_id' => $vehicle->id, + 'user_id' => $record->user_id, + 'created_at' => $record->created_at->toDateTimeString(), + ], + 'vehicle' => [ + 'id' => $vehicle->id, + 'placa' => $vehicle->placa, + 'numero_serie' => $vehicle->numero_serie, + 'marca' => $vehicle->marca, + 'modelo' => $vehicle->modelo, + 'color' => $vehicle->color, + ], + 'owner' => [ + 'id' => $owner->id, + 'full_name' => $owner->full_name, + 'rfc' => $owner->rfc, + ], + 'tag' => [ + 'id' => $tag->id, + 'folio' => $tag->folio, + ], + 'files' => $uploadedFiles, + 'total_files' => count($uploadedFiles), + ]); + } catch (\Exception $e) { + DB::rollBack(); + + Log::error('Error en inscripcionVehiculo: ' . $e->getMessage(), [ + 'folio' => $folio ?? null, + 'tag_id' => $tagId ?? null, + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Error al procesar la inscripción del vehículo', + 'error' => $e->getMessage(), + ]); + } + } + + private function checkIfStolen(string $folio): bool + { + // Aquí api servicio de REPUVE Nacional + // simulamos con random + return (bool) rand(0, 1); + } + + private function sendToRepuveNacional(string $folio, int $tagId, array $vehicleData): array + { + // Enviar datos a API Repuve Nacional + // Aquí se haría la llamada real a la API de Repuve Nacional + // Por ahora simulamos respuestas aleatorias usando la tabla errors + + $hasError = (bool) rand(0, 1); + + if ($hasError) { + // Obtener un error aleatorio de la tabla errors + $error = Error::inRandomOrder()->first(); + + if (!$error) { + // Si no hay errores en la tabla, retornar error genérico + return [ + 'has_error' => true, + 'error_code' => 'ERR_UNKNOWN', + 'error_message' => 'No hay errores registrados en el catálogo', + 'timestamp' => now()->toDateTimeString(), + 'folio' => $folio, + 'tag_id' => $tagId, + 'response_data' => null, + ]; + } + + return [ + 'has_error' => true, + 'error_code' => $error->code, + 'error_message' => $error->description, + 'timestamp' => now()->toDateTimeString(), + 'folio' => $folio, + 'tag_id' => $tagId, + 'response_data' => null, + ]; + } + + // Respuesta exitosa + return [ + 'has_error' => false, + 'error_code' => null, + 'error_message' => null, + 'timestamp' => now()->toDateTimeString(), + 'folio' => $folio, + 'tag_id' => $tagId, + 'response_data' => [ + 'status' => 'success', + 'repuve_id' => 'REPUVE-' . strtoupper(uniqid()), + 'validated' => true, + ], + ]; + } + + public function searchRecord(Request $request) + { + $request->validate([ + 'folio' => 'nullable|string', + 'placa' => 'nullable|string', + 'niv' => 'nullable|string', + 'per_page' => 'nullable|integer|min:1|max:20', + ], [ + 'required_without_all' => 'Debe proporcionar al menos uno de los siguientes: folio, placa o NIV.' + ]); + + if (!$request->filled('folio') && !$request->filled('placa') && !$request->filled('niv')) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Debe proporcionar al menos uno de los siguientes parámetros: folio, placa o niv.' + ]); + } + + $query = Record::with(['vehicle.owner', 'vehicle.tag', 'files', 'user', 'error'])->orderBy('created_at', 'desc'); + + if ($request->filled('folio')) { + $query->where('folio', 'LIKE', '%' . $request->input('folio') . '%'); + } elseif ($request->filled('placa')) { + $query->whereHas('vehicle', function ($q) use ($request) { + $q->where('placa', 'LIKE', '%' . $request->input('placa') . '%'); + }); + } elseif ($request->filled('niv')) { + $query->whereHas('vehicle', function ($q) use ($request) { + $q->where('numero_serie', 'LIKE', '%' . $request->input('niv') . '%'); + }); + } + + $perPage = $request->input('per_page', 20); + $records = $query->paginate($perPage); + + if ($records->isEmpty()) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'No se encontraron expedientes con los criterios proporcionados.', + 'records' => [], + 'pagination' => [ + 'current_page' => 1, + 'total_pages' => 0, + 'total_records' => 0, + 'per_page' => $perPage, + ], + ]); + } + return ApiResponse::OK->response([ + 'message' => 'Expedientes encontrados exitosamente', + 'records' => $records->map(function ($record) { + return [ + 'id' => $record->id, + 'folio' => $record->folio, + 'vehicle' => [ + 'id' => $record->vehicle->id, + 'placa' => $record->vehicle->placa, + 'numero_serie' => $record->vehicle->numero_serie, + 'marca' => $record->vehicle->marca, + 'modelo' => $record->vehicle->modelo, + 'color' => $record->vehicle->color, + 'tipo' => $record->vehicle->tipo, + ], + 'owner' => [ + 'id' => $record->vehicle->owner->id, + 'full_name' => $record->vehicle->owner->full_name, + 'rfc' => $record->vehicle->owner->rfc, + ], + 'files' => $record->files->map(function ($file) { + return [ + 'id' => $file->id, + 'name' => $file->name, + 'path' => $file->path, + 'url' => $file->url, + 'md5' => $file->md5, + ]; + }), + ]; + }), + 'pagination' => [ + 'current_page' => $records->currentPage(), + 'total_pages' => $records->lastPage(), + 'total_records' => $records->total(), + 'per_page' => $records->perPage(), + 'from' => $records->firstItem(), + 'to' => $records->lastItem(), + ], + ]); + } + + private function getVehicle(): array + { + return [ + "ANIO_PLACA" => "2020", + "PLACA" => "WNU700B", + "NO_SERIE" => "LSGHD52H0ND032457", + "RFC" => "GME111116GJA", + "FOLIO" => "EXP-2025-201030", + "VIGENCIA" => "2025", + "FECHA_IMPRESION" => "10-01-2025", + "QR_HASH" => "Vu5TF4kYsbbltzjDdGQyenKfZoIk2wro34a5Gkh9JVh0CFxfPlrd92YEWK21JF.nLjQNyzKmqRvWYuPiS.kU7A--", + "VALIDO" => true, + "FOLIOTEMP" => false, + "NOMBRE" => "GOLSYSTEMS DE MEXICO S DE RL DE CV", + "NOMBRE2" => "GOLS*MS DXICOE RL*CV", + "MUNICIPIO" => "CENTRO", + "LOCALIDAD" => "VILLAHERMOSA", + "CALLE" => "C BUGAMBILIAS 118 ", + "CALLE2" => "C BU*ILIA*18 ", + "TIPO" => "SEDAN", + "TIPO_SERVICIO" => "PARTICULAR", + "MARCA" => "CHEVROLET G.M.C.", + "LINEA" => "AVEO", + "SUBLINEA" => "PAQ. \"A\" LS", + "MODELO" => 2022, + "NUMERO_SERIE" => "LSGHD52H0ND032457", + "NUMERO_MOTOR" => "H. EN WUHANLL,SGM", + "DESCRIPCION_ORIGEN" => "IMPORTADO", + "COLOR" => "BLANCO", + "CODIGO_POSTAL" => "86179", + "SERIE_FOLIO" => "D3962243", + "SFOLIO" => "3962243" + ]; + } + + private function getOwner(): array + { + return [ + 'name' => 'Nicolas', + 'paternal' => 'Hernandez', + 'maternal' => 'Castillo', + 'rfc' => 'HECN660509HTCRSC01', + 'curp' => 'HECN660509HTCRSC01', + 'address' => 'Fracc Pomoca, Calle Armadillo MZ9 LT28', + ]; + } + + /** + * Listar TAGs filtrados por status + */ + public function listTags(Request $request) + { + try { + $request->validate([ + 'status' => 'required|in:available,assigned,cancelled,lost', + 'per_page' => 'nullable|integer|min:1|max:100', + ], [ + 'status.required' => 'El parámetro status es requerido', + 'status.in' => 'El status debe ser uno de: available, assigned, cancelled, lost', + ]); + + $status = $request->input('status'); + $perPage = $request->input('per_page', 20); + + // Query base + $query = Tag::where('status', $status); + + // Cargar relaciones según el status + if ($status === 'assigned') { + // Si está asignado, cargar vehículo y propietario + $query->with(['vehicle.owner', 'package']); + } else { + // Si no está asignado, solo cargar paquete + $query->with('package'); + } + + // Ordenar por más reciente + $query->orderBy('created_at', 'desc'); + + // Paginar + $tags = $query->paginate($perPage); + + if ($tags->isEmpty()) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'No se encontraron tags con el status: ' . $status, + 'tags' => [], + 'pagination' => [ + 'current_page' => 1, + 'total_pages' => 0, + 'total' => 0, + 'per_page' => $perPage, + ], + ]); + } + + return ApiResponse::OK->response([ + 'message' => 'Tags encontrados exitosamente', + 'status_filter' => $status, + 'tags' => $tags->map(function ($tag) use ($status) { + $tagData = [ + 'id' => $tag->id, + 'folio' => $tag->folio, + 'status' => $tag->status, + 'package' => $tag->package ? [ + 'id' => $tag->package->id, + 'lot' => $tag->package->lot, + 'box_number' => $tag->package->box_number, + ] : null, + 'created_at' => $tag->created_at->format('Y-m-d H:i:s'), + 'updated_at' => $tag->updated_at->format('Y-m-d H:i:s'), + ]; + + // Si el tag está asignado, agregar información del vehículo + if ($status === 'assigned' && $tag->vehicle) { + $tagData['vehicle'] = [ + 'id' => $tag->vehicle->id, + 'placa' => $tag->vehicle->placa, + 'numero_serie' => $tag->vehicle->numero_serie, + 'marca' => $tag->vehicle->marca, + 'modelo' => $tag->vehicle->modelo, + 'color' => $tag->vehicle->color, + 'owner' => $tag->vehicle->owner ? [ + 'id' => $tag->vehicle->owner->id, + 'full_name' => $tag->vehicle->owner->full_name, + 'rfc' => $tag->vehicle->owner->rfc, + ] : null, + ]; + } else { + $tagData['vehicle'] = null; + } + + return $tagData; + }), + 'pagination' => [ + 'current_page' => $tags->currentPage(), + 'total_pages' => $tags->lastPage(), + 'total' => $tags->total(), + 'per_page' => $tags->perPage(), + 'from' => $tags->firstItem(), + 'to' => $tags->lastItem(), + ], + ]); + + } catch (\Illuminate\Validation\ValidationException $e) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Error de validación', + 'errors' => $e->errors(), + ]); + } catch (\Exception $e) { + Log::error('Error al listar tags: ' . $e->getMessage(), [ + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al listar tags', + 'error' => $e->getMessage(), + ]); + } + } + +} diff --git a/app/Http/Controllers/Repuve/ModuleController.php b/app/Http/Controllers/Repuve/ModuleController.php new file mode 100644 index 0000000..b39c83b --- /dev/null +++ b/app/Http/Controllers/Repuve/ModuleController.php @@ -0,0 +1,255 @@ +filled('name') && !$request->filled('municipality')) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Debe proporcionar al menos uno de los siguientes parámetros: nombre o municipio.' + ]); + } + + // Filtro por nombre + if ($request->filled('name')) { + $query->where('name', 'like', '%' . $request->input('name') . '%'); + } + + // Filtro por municipio + if ($request->filled('municipality')) { + $query->where('municipality', 'like', '%' . $request->input('municipality') . '%'); + } + + // Cargar relaciones para contar + $query->withCount(['packages']); + + // Ordenar + $sortBy = $request->input('sort_by', 'created_at'); + $sortOrder = $request->input('sort_order', 'desc'); + $query->orderBy($sortBy, $sortOrder); + + // Paginación + $perPage = $request->input('per_page', 20); + $modules = $query->paginate($perPage); + + return ApiResponse::OK->response([ + 'modules' => $modules->map(function ($module) { + return [ + 'id' => $module->id, + 'name' => $module->name, + 'municipality' => $module->municipality, + 'address' => $module->address, + 'colony' => $module->colony, + 'cp' => $module->cp, + 'longitude' => $module->longitude, + 'latitude' => $module->latitude, + 'status' => $module->status, + 'status_text' => $module->status ? 'Activo' : 'Inactivo', + 'packages_count' => $module->packages_count ?? 0, + 'created_at' => $module->created_at->format('Y-m-d H:i:s'), + 'updated_at' => $module->updated_at->format('Y-m-d H:i:s'), + ]; + }), + 'pagination' => [ + 'current_page' => $modules->currentPage(), + 'total_pages' => $modules->lastPage(), + 'per_page' => $modules->perPage(), + 'total' => $modules->total(), + 'from' => $modules->firstItem(), + 'to' => $modules->lastItem(), + ], + ]); + + } catch (\Exception $e) { + Log::error('Error al listar módulos: ' . $e->getMessage(), [ + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al listar módulos', + 'error' => $e->getMessage(), + ]); + } + } + + /** + * Crear un nuevo módulo + */ + public function store(ModuleStoreRequest $request) + { + try { + DB::beginTransaction(); + + // Crear el módulo + $module = Module::create([ + 'name' => $request->input('name'), + 'municipality' => $request->input('municipality'), + 'address' => $request->input('address'), + 'colony' => $request->input('colony'), + 'cp' => $request->input('cp'), + 'longitude' => $request->input('longitude'), + 'latitude' => $request->input('latitude'), + 'status' => $request->input('status', true), // Por defecto activo + ]); + + DB::commit(); + + return ApiResponse::CREATED->response([ + 'message' => 'Módulo creado exitosamente', + 'module' => [ + 'id' => $module->id, + 'name' => $module->name, + 'municipality' => $module->municipality, + 'address' => $module->address, + 'colony' => $module->colony, + 'cp' => $module->cp, + 'longitude' => $module->longitude, + 'latitude' => $module->latitude, + 'status' => $module->status, + 'status_text' => $module->status ? 'Activo' : 'Inactivo', + 'created_at' => $module->created_at->format('Y-m-d H:i:s'), + ], + ]); + + } catch (\Exception $e) { + DB::rollBack(); + Log::error('Error al crear módulo: ' . $e->getMessage(), [ + 'request' => $request->all(), + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al crear módulo', + 'error' => $e->getMessage(), + ]); + } + } + + /** + * Actualizar un módulo existente + */ + public function update(ModuleUpdateRequest $request, int $id) + { + try { + $module = Module::findOrFail($id); + + DB::beginTransaction(); + + // Actualizar solo los campos que vienen en el request + $module->update($request->only([ + 'name', + 'municipality', + 'address', + 'colony', + 'cp', + 'longitude', + 'latitude', + 'status', + ])); + + DB::commit(); + + return ApiResponse::OK->response([ + 'message' => 'Módulo actualizado exitosamente', + 'module' => [ + 'id' => $module->id, + 'name' => $module->name, + 'municipality' => $module->municipality, + 'address' => $module->address, + 'colony' => $module->colony, + 'cp' => $module->cp, + 'longitude' => $module->longitude, + 'latitude' => $module->latitude, + 'status' => $module->status, + 'status_text' => $module->status ? 'Activo' : 'Inactivo', + 'updated_at' => $module->updated_at->format('Y-m-d H:i:s'), + ], + ]); + + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'Módulo no encontrado', + ]); + } catch (\Exception $e) { + DB::rollBack(); + Log::error('Error al actualizar módulo: ' . $e->getMessage(), [ + 'module_id' => $id, + 'request' => $request->all(), + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al actualizar módulo', + 'error' => $e->getMessage(), + ]); + } + } + + /** + * Cambiar solo el status de un módulo + */ + public function toggleStatus(int $id) + { + try { + $module = Module::findOrFail($id); + + DB::beginTransaction(); + + // Cambiar el status al valor opuesto + $module->update([ + 'status' => !$module->status, + ]); + + DB::commit(); + + return ApiResponse::OK->response([ + 'message' => $module->status + ? 'Módulo activado exitosamente' + : 'Módulo desactivado exitosamente', + 'module' => [ + 'id' => $module->id, + 'name' => $module->name, + 'status' => $module->status, + 'status_text' => $module->status ? 'Activo' : 'Inactivo', + 'updated_at' => $module->updated_at->format('Y-m-d H:i:s'), + ], + ]); + + } catch (ModelNotFoundException $e) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'Módulo no encontrado', + ]); + } catch (\Exception $e) { + DB::rollBack(); + Log::error('Error al cambiar status del módulo: ' . $e->getMessage(), [ + 'module_id' => $id, + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al cambiar status del módulo', + 'error' => $e->getMessage(), + ]); + } + } +} diff --git a/app/Http/Controllers/Repuve/RepuveController.php b/app/Http/Controllers/Repuve/RepuveController.php deleted file mode 100644 index 08904a5..0000000 --- a/app/Http/Controllers/Repuve/RepuveController.php +++ /dev/null @@ -1,460 +0,0 @@ -input('folio'); - - // Simular consulta de robo - $isStolen = $this->checkIfStolen($folio); - - // Si está robado, detener el flujo - if ($isStolen) { - return ApiResponse::FORBIDDEN->response([ - 'folio' => $folio, - 'stolen' => true, - 'message' => 'El vehículo reporta robo. No se puede continuar con la inscripción.', - ]); - } - - // Iniciar transacción - DB::beginTransaction(); - - // Obtener datos del vehículo y propietario - $vehicleData = $this->getVehicle(); - $ownerData = $this->getOwner(); - - //Crear propietario - $owner = Owner::updateOrCreate( - ['rfc' => $ownerData['rfc']], - [ - 'name' => $ownerData['name'], - 'paternal' => $ownerData['paternal'], - 'maternal' => $ownerData['maternal'], - 'curp' => $ownerData['curp'], - 'address' => $ownerData['address'], - ] - ); - - //Crear o actualizar vehículo - $vehicle = Vehicle::updateOrCreate( - ['placa' => $vehicleData['PLACA']], - [ - 'anio_placa' => $vehicleData['ANIO_PLACA'], - 'placa' => $vehicleData['PLACA'], - 'numero_serie' => $vehicleData['NO_SERIE'], - 'rfc' => $vehicleData['RFC'], - 'folio' => $vehicleData['FOLIO'], - 'vigencia' => $vehicleData['VIGENCIA'], - 'fecha_impresion' => $vehicleData['FECHA_IMPRESION'], - 'qr_hash' => $vehicleData['QR_HASH'], - 'valido' => $vehicleData['VALIDO'], - 'foliotemp' => $vehicleData['FOLIOTEMP'], - 'nombre' => $vehicleData['NOMBRE'], - 'nombre2' => $vehicleData['NOMBRE2'], - 'municipio' => $vehicleData['MUNICIPIO'], - 'localidad' => $vehicleData['LOCALIDAD'], - 'calle' => $vehicleData['CALLE'], - 'calle2' => $vehicleData['CALLE2'], - 'tipo' => $vehicleData['TIPO'], - 'tipo_servicio' => $vehicleData['TIPO_SERVICIO'], - 'marca' => $vehicleData['MARCA'], - 'linea' => $vehicleData['LINEA'], - 'sublinea' => $vehicleData['SUBLINEA'], - 'modelo' => $vehicleData['MODELO'], - 'numero_motor' => $vehicleData['NUMERO_MOTOR'], - 'descripcion_origen' => $vehicleData['DESCRIPCION_ORIGEN'], - 'color' => $vehicleData['COLOR'], - 'codigo_postal' => $vehicleData['CODIGO_POSTAL'], - 'serie_folio' => $vehicleData['SERIE_FOLIO'], - 'sfolio' => $vehicleData['SFOLIO'], - 'nrpv' => $vehicleData['NUMERO_SERIE'], - 'owner_id' => $owner->id, - ] - ); - - //Crear registro (expediente) - $record = Record::create([ - 'folio' => $folio, - 'vehicle_id' => $vehicle->id, - 'user_id' => auth()->id(), - ]); - - //Procesar y guardar archivos si existen - $uploadedFiles = []; - if ($request->hasFile('files')) { - $files = $request->file('files'); - $fileNames = $request->input('names', []); - - foreach ($files as $index => $file) { - // Generar nombre único - $fileName = uniqid() . '_' . time() . '_' . $file->getClientOriginalName(); - - // Guardar archivos - $path = $file->storeAs('records', $fileName, 'public'); - - // Calcular MD5 - $md5 = md5_file($file->getRealPath()); - - // Crear registro en BD - $fileRecord = File::create([ - 'name' => $fileNames[$index] ?? "Archivo " . ($index + 1), - 'path' => $path, - 'md5' => $md5, - 'record_id' => $record->id, - ]); - - $uploadedFiles[] = [ - 'id' => $fileRecord->id, - 'name' => $fileRecord->name, - 'path' => $fileRecord->path, - 'url' => $fileRecord->url, - ]; - } - } - - // Confirmar transacción - DB::commit(); - - // Responder con éxito - return ApiResponse::OK->response([ - 'folio' => $folio, - 'stolen' => false, - 'message' => 'Vehículo inscrito exitosamente', - 'record' => [ - 'id' => $record->id, - 'folio' => $record->folio, - 'vehicle_id' => $vehicle->id, - 'user_id' => $record->user_id, - 'created_at' => $record->created_at->toDateTimeString(), - ], - 'vehicle' => [ - 'id' => $vehicle->id, - 'placa' => $vehicle->placa, - 'numero_serie' => $vehicle->numero_serie, - 'marca' => $vehicle->marca, - 'modelo' => $vehicle->modelo, - 'color' => $vehicle->color, - ], - 'owner' => [ - 'id' => $owner->id, - 'full_name' => $owner->full_name, - 'rfc' => $owner->rfc, - ], - 'files' => $uploadedFiles, - 'total_files' => count($uploadedFiles), - ]); - } catch (\Exception $e) { - // Revertir transacción en caso de error - DB::rollBack(); - - // Log del error - Log::error('Error en inscripcionVehiculo: ' . $e->getMessage(), [ - 'folio' => $folio ?? null, - 'trace' => $e->getTraceAsString() - ]); - - return ApiResponse::BAD_REQUEST->response([ - 'message' => 'Error al procesar la inscripción del vehículo', - 'error' => $e->getMessage(), - ]); - } - } - - private function checkIfStolen(string $folio): bool - { - // Aquí api servicio de REPUVE - // simulamos con random - return (bool) rand(0, 1); - } - - public function consultaExpediente(Request $request) - { - $request->validate([ - 'folio' => 'nullable|string', - 'placa' => 'nullable|string', - 'niv' => 'nullable|string', - ], [ - 'required_without_all' => 'Debe proporcionar al menos uno de los siguientes: folio, placa o NIV.' - ]); - - if (!$request->filled('folio') && !$request->filled('placa') && !$request->filled('niv')) { - return ApiResponse::BAD_REQUEST->response([ - 'message' => 'Debe proporcionar al menos uno de los siguientes parámetros: folio, placa o niv.' - ]); - } - - $query = Record::with(['vehicle.owner', 'vehicle.tag', 'files', 'user']); - - if ($request->filled('folio')) { - $query->where('folio', $request->input('folio')); - } elseif ($request->filled('placa')) { - $query->whereHas('vehicle', function ($q) use ($request) { - $q->where('placa', $request->input('placa')); - }); - } elseif ($request->filled('niv')) { - $query->whereHas('vehicle', function ($q) use ($request) { - $q->where('numero_serie', $request->input('niv')); - }); - } - - $record = $query->first(); - - if (!$record) { - return ApiResponse::NOT_FOUND->response([ - 'message' => 'No se encontró ningún expediente con el folio proporcionado.' - ]); - } - - return ApiResponse::OK->response([ - 'record' => [ - 'id' => $record->id, - 'folio' => $record->folio, - 'vehicle' => $record->vehicle, - 'owner' => $record->vehicle->owner, - 'tag' => $record->vehicle->tag, - 'files' => $record->files, - 'user' => $record->user, - 'created_at' => $record->created_at->toDateTimeString(), - ] - ]); - } - - public function actualizarVehiculo(VehicleUpdateRequest $request, $recordId) - { - try { - // Buscar el registro existente - $record = Record::with(['vehicle.owner'])->findOrFail($recordId); - - // Iniciar transacción - DB::beginTransaction(); - - $vehicleData = $this->getVehicle2(); - $ownerData = $this->getOwner(); - - // Actualizar propietario - $owner = Owner::updateOrCreate( - ['rfc' => $ownerData['rfc']], - [ - 'name' => $ownerData['name'], - 'paternal' => $ownerData['paternal'], - 'maternal' => $ownerData['maternal'], - 'curp' => $ownerData['curp'], - 'address' => $ownerData['address'], - ] - ); - - // Actualizar vehículo - $record->vehicle->update([ - 'anio_placa' => $vehicleData['ANIO_PLACA'], - 'placa' => $vehicleData['PLACA'], - 'numero_serie' => $vehicleData['NO_SERIE'], - 'rfc' => $vehicleData['RFC'], - 'folio' => $vehicleData['FOLIO'], - 'vigencia' => $vehicleData['VIGENCIA'], - 'fecha_impresion' => $vehicleData['FECHA_IMPRESION'], - 'qr_hash' => $vehicleData['QR_HASH'], - 'valido' => $vehicleData['VALIDO'], - 'foliotemp' => $vehicleData['FOLIOTEMP'], - 'nombre' => $vehicleData['NOMBRE'], - 'nombre2' => $vehicleData['NOMBRE2'], - 'municipio' => $vehicleData['MUNICIPIO'], - 'localidad' => $vehicleData['LOCALIDAD'], - 'calle' => $vehicleData['CALLE'], - 'calle2' => $vehicleData['CALLE2'], - 'tipo' => $vehicleData['TIPO'], - 'tipo_servicio' => $vehicleData['TIPO_SERVICIO'], - 'marca' => $vehicleData['MARCA'], - 'linea' => $vehicleData['LINEA'], - 'sublinea' => $vehicleData['SUBLINEA'], - 'modelo' => $vehicleData['MODELO'], - 'numero_motor' => $vehicleData['NUMERO_MOTOR'], - 'descripcion_origen' => $vehicleData['DESCRIPCION_ORIGEN'], - 'color' => $vehicleData['COLOR'], - 'codigo_postal' => $vehicleData['CODIGO_POSTAL'], - 'serie_folio' => $vehicleData['SERIE_FOLIO'], - 'sfolio' => $vehicleData['SFOLIO'], - 'nrpv' => $vehicleData['NUMERO_SERIE'], - 'owner_id' => $owner->id, - ]); - - // Procesar nuevos archivos si existen - $uploadedFiles = []; - if ($request->hasFile('files')) { - $files = $request->file('files'); - $fileNames = $request->input('names', []); - - foreach ($files as $index => $file) { - // Generar nombre - $fileName = uniqid() . '_' . time() . '_' . $file->getClientOriginalName(); - - // Guardar archivos - $path = $file->storeAs('records', $fileName, 'public'); - - // Calcular MD5 - $md5 = md5_file($file->getRealPath()); - - // Crear registro en BD - $fileRecord = File::create([ - 'name' => $fileNames[$index] ?? "Archivo " . ($index + 1), - 'path' => $path, - 'md5' => $md5, - 'record_id' => $record->id, - ]); - - $uploadedFiles[] = [ - 'id' => $fileRecord->id, - 'name' => $fileRecord->name, - 'path' => $fileRecord->path, - 'url' => $fileRecord->url, - ]; - } - } - - // Confirmar transacción - DB::commit(); - - // Responder con éxito - return ApiResponse::OK->response([ - 'message' => 'Vehículo actualizado exitosamente', - 'record' => [ - 'id' => $record->id, - 'folio' => $record->folio, - 'vehicle_id' => $record->vehicle->id, - 'updated_at' => $record->updated_at->toDateTimeString(), - ], - 'vehicle' => [ - 'id' => $record->vehicle->id, - 'placa' => $record->vehicle->placa, - 'numero_serie' => $record->vehicle->numero_serie, - 'marca' => $record->vehicle->marca, - 'modelo' => $record->vehicle->modelo, - ], - 'owner' => [ - 'id' => $owner->id, - 'full_name' => $owner->full_name, - 'rfc' => $owner->rfc, - ], - 'new_files' => $uploadedFiles, - 'total_new_files' => count($uploadedFiles), - ]); - } catch (\Exception $e) { - DB::rollBack(); - - Log::error('Error en actualizarVehiculo: ' . $e->getMessage(), [ - 'record_id' => $recordId ?? null, - 'trace' => $e->getTraceAsString() - ]); - - return ApiResponse::BAD_REQUEST->response([ - 'message' => 'Error al actualizar el vehículo', - 'error' => $e->getMessage(), - ]); - } - } - - private function getVehicle(): array - { - return [ - "ANIO_PLACA" => "2020", - "PLACA" => "WNU700B", - "NO_SERIE" => "LSGHD52H0ND032457", - "RFC" => "GME111116GJA", - "FOLIO" => "EXP-2025-201030", - "VIGENCIA" => "2025", - "FECHA_IMPRESION" => "10-01-2025", - "QR_HASH" => "Vu5TF4kYsbbltzjDdGQyenKfZoIk2wro34a5Gkh9JVh0CFxfPlrd92YEWK21JF.nLjQNyzKmqRvWYuPiS.kU7A--", - "VALIDO" => true, - "FOLIOTEMP" => false, - "NOMBRE" => "GOLSYSTEMS DE MEXICO S DE RL DE CV", - "NOMBRE2" => "GOLS*MS DXICOE RL*CV", - "MUNICIPIO" => "CENTRO", - "LOCALIDAD" => "VILLAHERMOSA", - "CALLE" => "C BUGAMBILIAS 118 ", - "CALLE2" => "C BU*ILIA*18 ", - "TIPO" => "SEDAN", - "TIPO_SERVICIO" => "PARTICULAR", - "MARCA" => "CHEVROLET G.M.C.", - "LINEA" => "AVEO", - "SUBLINEA" => "PAQ. \"A\" LS", - "MODELO" => 2022, - "NUMERO_SERIE" => "LSGHD52H0ND032457", - "NUMERO_MOTOR" => "H. EN WUHANLL,SGM", - "DESCRIPCION_ORIGEN" => "IMPORTADO", - "COLOR" => "BLANCO", - "CODIGO_POSTAL" => "86179", - "SERIE_FOLIO" => "D3962243", - "SFOLIO" => "3962243" - ]; - } - - private function getVehicle2(): array - { - return [ - "ANIO_PLACA" => "2027", - "PLACA" => "WNU700Z", - "NO_SERIE" => "EXP-2025-201030", - "RFC" => "GME111116GJA", - "FOLIO" => "EXP-2025-201030", - "VIGENCIA" => "2026", - "FECHA_IMPRESION" => "10-01-2025", - "QR_HASH" => "Vu5TF4kYsbbltzjDdGQyenKfZoIk2wro34a5Gkh9JVh0CFxfPlrd92YEWK21JF.nLjQNyzKmqRvWYuPiS.kU7A--", - "VALIDO" => true, - "FOLIOTEMP" => false, - "NOMBRE" => "GOLSYSTEMS DE MEXICO S DE RL DE CV", - "NOMBRE2" => "GOLS*MS DXICOE RL*CV", - "MUNICIPIO" => "CENTRO", - "LOCALIDAD" => "VILLAHERMOSA", - "CALLE" => "C BUGAMBILIAS 119 ", - "CALLE2" => "C BU*ILIA*18 ", - "TIPO" => "SEDAN", - "TIPO_SERVICIO" => "PARTICULAR", - "MARCA" => "CHEVROLET G.M.C.", - "LINEA" => "AVEO", - "SUBLINEA" => "PAQ. \"A\" LS", - "MODELO" => 2023, - "NUMERO_SERIE" => "EXP-2025-201030", - "NUMERO_MOTOR" => "H. EN WUHANLL,SGM", - "DESCRIPCION_ORIGEN" => "IMPORTADO", - "COLOR" => "AZUL", - "CODIGO_POSTAL" => "86181", - "SERIE_FOLIO" => "D3962242", - "SFOLIO" => "EXP-2025-201030" - ]; - } - - private function getOwner(): array - { - return [ - 'name' => 'Nicolas', - 'paternal' => 'Hernandez', - 'maternal' => 'Castillo', - 'rfc' => 'HECN660509HTCRSC01', - 'curp' => 'HECN660509HTCRSC01', - 'address' => 'Fracc Pomoca, Calle Armadillo MZ9 LT28', - ]; - } -} diff --git a/app/Http/Controllers/Repuve/UpdateController.php b/app/Http/Controllers/Repuve/UpdateController.php new file mode 100644 index 0000000..537fd1a --- /dev/null +++ b/app/Http/Controllers/Repuve/UpdateController.php @@ -0,0 +1,348 @@ +input('folio'); + $tagId = $request->input('tag_id'); + + // Buscar vehículo por folio y tag + $tag = Tag::with('vehicle.owner')->findOrFail($tagId); + + // Validar que el tag corresponda al folio + if ($tag->folio !== $folio) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'El folio no coincide con el tag RFID proporcionado', + 'folio_request' => $folio, + 'folio_tag' => $tag->folio, + ]); + } + + $vehicle = $tag->vehicle; + + if (!$vehicle) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'No se encontró un vehículo asociado al tag', + ]); + } + + // Consultar API Repuve Nacional (verificar robo) + $isStolen = $this->checkIfStolen($folio); + + if ($isStolen) { + return ApiResponse::FORBIDDEN->response([ + 'folio' => $folio, + 'tag_id' => $tagId, + 'stolen' => true, + 'message' => 'El vehículo reporta robo. No se puede continuar con la actualización.', + ]); + } + + // Iniciar transacción + DB::beginTransaction(); + + // Obtener datos del vehículo de API Estatal + $vehicleData = $this->getVehicle2(); + $ownerData = $this->getOwner(); + + // Actualizar propietario + $owner = Owner::updateOrCreate( + ['rfc' => $ownerData['rfc']], + [ + 'name' => $ownerData['name'], + 'paternal' => $ownerData['paternal'], + 'maternal' => $ownerData['maternal'], + 'curp' => $ownerData['curp'], + 'address' => $ownerData['address'], + ] + ); + + // Actualizar vehículo + $vehicle->update([ + 'anio_placa' => $vehicleData['ANIO_PLACA'], + // NO actualizar 'placa' - es UNIQUE + // NO actualizar 'numero_serie' - es UNIQUE (NIV/VIN) + 'rfc' => $vehicleData['RFC'], + 'folio' => $folio, + 'vigencia' => $vehicleData['VIGENCIA'], + 'fecha_impresion' => $vehicleData['FECHA_IMPRESION'], + 'qr_hash' => $vehicleData['QR_HASH'], + 'valido' => $vehicleData['VALIDO'], + 'nombre' => $vehicleData['NOMBRE'], + 'nombre2' => $vehicleData['NOMBRE2'], + 'municipio' => $vehicleData['MUNICIPIO'], + 'localidad' => $vehicleData['LOCALIDAD'], + 'calle' => $vehicleData['CALLE'], + 'calle2' => $vehicleData['CALLE2'], + 'tipo' => $vehicleData['TIPO'], + 'tipo_servicio' => $vehicleData['TIPO_SERVICIO'], + 'marca' => $vehicleData['MARCA'], + 'linea' => $vehicleData['LINEA'], + 'sublinea' => $vehicleData['SUBLINEA'], + 'modelo' => $vehicleData['MODELO'], + 'numero_motor' => $vehicleData['NUMERO_MOTOR'], + 'descripcion_origen' => $vehicleData['DESCRIPCION_ORIGEN'], + 'color' => $vehicleData['COLOR'], + 'codigo_postal' => $vehicleData['CODIGO_POSTAL'], + // NO actualizar 'serie_folio' - es UNIQUE + // NO actualizar 'sfolio' - es UNIQUE + // NO actualizar 'nrpv' - es UNIQUE (mantener el original) + 'owner_id' => $owner->id, + ]); + + $record = Record::firstOrCreate( + ['vehicle_id' => $vehicle->id], + [ + 'folio' => $folio, + 'user_id' => Auth::id(), + ] + ); + + // Enviar datos a API Repuve Nacional + $apiResponse = $this->sendToRepuveNacional($folio, $tagId, $vehicleData); + + // Procesar respuesta de la API + if (isset($apiResponse['has_error']) && $apiResponse['has_error']) { + // Si hay error, busca bd + $error = Error::where('code', $apiResponse['error_code'])->first(); + + if (!$error) { + DB::rollBack(); + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Código de error no encontrado en el catálogo', + 'error_code' => $apiResponse['error_code'], + 'error_message' => $apiResponse['error_message'], + ]); + } + + // guarda error + $record->update([ + 'error_id' => $error->id, + 'api_response' => $apiResponse, // Guarda respuesta con error + 'error_occurred_at' => now(), + ]); + + DB::commit(); + + // Retornar datos por error para que el usuario corrija + // Al volver a llamar esta función con datos corregidos, se repetirá el ciclo + return ApiResponse::OK->response([ + 'message' => 'Datos guardados con error. Corrija los datos y vuelva a enviar.', + 'has_error' => true, + 'can_retry' => true, + 'error' => [ + 'code' => $error->code, + 'description' => $error->description, + 'occurred_at' => $record->error_occurred_at->toDateTimeString(), + ], + 'record' => [ + 'id' => $record->id, + 'folio' => $record->folio, + ], + 'vehicle' => $vehicleData, // Retorna datos para edición + 'owner' => $ownerData, + ]); + } + + // Si NO hay error, guarda registro OK y quita errores previos + $record->update([ + 'error_id' => null, + 'api_response' => $apiResponse, + 'error_occurred_at' => null, + ]); + + // Procesar archivos si existen + $uploadedFiles = []; + if ($request->hasFile('files')) { + $files = $request->file('files'); + $fileNames = $request->input('names', []); + + foreach ($files as $index => $file) { + $fileName = uniqid() . '_' . time() . '_' . $file->getClientOriginalName(); + $path = $file->storeAs('records', $fileName, 'public'); + $md5 = md5_file($file->getRealPath()); + + $fileRecord = File::create([ + 'name' => $fileNames[$index] ?? "Archivo " . ($index + 1), + 'path' => $path, + 'md5' => $md5, + 'record_id' => $record->id, + ]); + + $uploadedFiles[] = [ + 'id' => $fileRecord->id, + 'name' => $fileRecord->name, + 'path' => $fileRecord->path, + 'url' => $fileRecord->url, + ]; + } + } + + DB::commit(); + + return ApiResponse::OK->response([ + 'message' => 'Vehículo actualizado exitosamente', + 'has_error' => false, + 'record' => [ + 'id' => $record->id, + 'folio' => $record->folio, + 'updated_at' => $record->updated_at->toDateTimeString(), + ], + 'vehicle' => [ + 'id' => $vehicle->id, + 'placa' => $vehicle->placa, + 'numero_serie' => $vehicle->numero_serie, + 'marca' => $vehicle->marca, + 'modelo' => $vehicle->modelo, + ], + 'owner' => [ + 'id' => $owner->id, + 'full_name' => $owner->full_name, + 'rfc' => $owner->rfc, + ], + 'tag' => [ + 'id' => $tag->id, + 'folio' => $tag->folio, + ], + 'new_files' => $uploadedFiles, + 'total_new_files' => count($uploadedFiles), + ]); + + } catch (\Exception $e) { + DB::rollBack(); + + Log::error('Error en actualizarVehiculo: ' . $e->getMessage(), [ + 'folio' => $folio ?? null, + 'tag_id' => $tagId ?? null, + 'trace' => $e->getTraceAsString() + ]); + + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Error al actualizar el vehículo', + 'error' => $e->getMessage(), + ]); + } + } + + private function checkIfStolen(string $folio): bool + { + // Aquí api servicio de REPUVE Nacional + // simulamos con random + return (bool) rand(0, 1); + } + + private function sendToRepuveNacional(string $folio, int $tagId, array $vehicleData): array + { + // Enviar datos a API Repuve Nacional + // Aquí se haría la llamada real a la API de Repuve Nacional + // Por ahora simulamos respuestas aleatorias usando la tabla errors + + $hasError = (bool) rand(0, 1); + + if ($hasError) { + // Obtener un error aleatorio de la tabla errors + $error = Error::inRandomOrder()->first(); + + if (!$error) { + // Si no hay errores en la tabla, retornar error genérico + return [ + 'has_error' => true, + 'error_code' => 'ERR_UNKNOWN', + 'error_message' => 'No hay errores registrados en el catálogo', + 'timestamp' => now()->toDateTimeString(), + 'folio' => $folio, + 'tag_id' => $tagId, + 'response_data' => null, + ]; + } + + return [ + 'has_error' => true, + 'error_code' => $error->code, + 'error_message' => $error->description, + 'timestamp' => now()->toDateTimeString(), + 'folio' => $folio, + 'tag_id' => $tagId, + 'response_data' => null, + ]; + } + + // Respuesta exitosa + return [ + 'has_error' => false, + 'error_code' => null, + 'error_message' => null, + 'timestamp' => now()->toDateTimeString(), + 'folio' => $folio, + 'tag_id' => $tagId, + 'response_data' => [ + 'status' => 'success', + 'repuve_id' => 'REPUVE-' . strtoupper(uniqid()), + 'validated' => true, + ], + ]; + } + + private function getVehicle2(): array + { + return [ + "ANIO_PLACA" => "2027", + "PLACA" => "WNU730X", + "NO_SERIE" => "EXP-2025-201030", + "RFC" => "GME111116GJA", + "FOLIO" => "EXP-2025-201030", + "VIGENCIA" => "2026", + "FECHA_IMPRESION" => "10-01-2025", + "QR_HASH" => "Vu5TF4kYsbbltzjDdGQyenKfZoIk2wro34a5Gkh9JVh0CFxfPlrd92YEWK21JF.nLjQNyzKmqRvWYuPiS.kU7A--", + "VALIDO" => true, + "NOMBRE" => "GOLSYSTEMS DE MEXICO S DE RL DE CV", + "NOMBRE2" => "GOLS*MS DXICOE RL*CV", + "MUNICIPIO" => "CENTRO", + "LOCALIDAD" => "VILLAHERMOSA", + "CALLE" => "C BUGAMBILIAS 119 ", + "CALLE2" => "C BU*ILIA*18 ", + "TIPO" => "SEDAN", + "TIPO_SERVICIO" => "PARTICULAR", + "MARCA" => "CHEVROLET G.M.C.", + "LINEA" => "AVEO", + "SUBLINEA" => "PAQ. \"A\" LS", + "MODELO" => 2023, + "NUMERO_SERIE" => "EXP-2025-201030", + "NUMERO_MOTOR" => "H. EN WUHANLL,SGM", + "DESCRIPCION_ORIGEN" => "IMPORTADO", + "COLOR" => "AZUL", + "CODIGO_POSTAL" => "86181", + "SERIE_FOLIO" => "D3962242", + "SFOLIO" => "EXP-2025-201030" + ]; + } + + private function getOwner(): array + { + return [ + 'name' => 'Nicolas', + 'paternal' => 'Hernandez', + 'maternal' => 'Castillo', + 'rfc' => 'HECN660509HTCRSC01', + 'curp' => 'HECN660509HTCRSC01', + 'address' => 'Fracc Pomoca, Calle Armadillo MZ9 LT28', + ]; + } +} diff --git a/app/Http/Requests/Repuve/ModuleStoreRequest.php b/app/Http/Requests/Repuve/ModuleStoreRequest.php new file mode 100644 index 0000000..6558f6f --- /dev/null +++ b/app/Http/Requests/Repuve/ModuleStoreRequest.php @@ -0,0 +1,52 @@ + ['required', 'string', 'max:255'], + 'municipality' => ['required', 'string', 'max:100'], + 'address' => ['required', 'string', 'max:255'], + 'colony' => ['required', 'string', 'max:100'], + 'cp' => ['nullable', 'string', 'max:10'], + 'longitude' => ['nullable', 'numeric', 'between:-180,180'], + 'latitude' => ['nullable', 'numeric', 'between:-90,90'], + 'status' => ['nullable', 'boolean'], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'El nombre del módulo es requerido', + 'name.string' => 'El nombre debe ser una cadena de texto', + 'name.max' => 'El nombre no debe superar los 255 caracteres', + 'municipality.required' => 'El municipio es requerido', + 'municipality.string' => 'El municipio debe ser una cadena de texto', + 'municipality.max' => 'El municipio no debe superar los 100 caracteres', + 'address.required' => 'La dirección es requerida', + 'address.string' => 'La dirección debe ser una cadena de texto', + 'address.max' => 'La dirección no debe superar los 255 caracteres', + 'colony.required' => 'La colonia es requerida', + 'colony.string' => 'La colonia debe ser una cadena de texto', + 'colony.max' => 'La colonia no debe superar los 100 caracteres', + 'cp.string' => 'El código postal debe ser una cadena de texto', + 'cp.max' => 'El código postal no debe superar los 10 caracteres', + 'longitude.numeric' => 'La longitud debe ser un número', + 'longitude.between' => 'La longitud debe estar entre -180 y 180', + 'latitude.numeric' => 'La latitud debe ser un número', + 'latitude.between' => 'La latitud debe estar entre -90 y 90', + 'status.boolean' => 'El status debe ser un valor booleano', + ]; + } +} diff --git a/app/Http/Requests/Repuve/ModuleUpdateRequest.php b/app/Http/Requests/Repuve/ModuleUpdateRequest.php new file mode 100644 index 0000000..12a5916 --- /dev/null +++ b/app/Http/Requests/Repuve/ModuleUpdateRequest.php @@ -0,0 +1,48 @@ + ['nullable', 'string', 'max:255'], + 'municipality' => ['nullable', 'string', 'max:100'], + 'address' => ['nullable', 'string', 'max:255'], + 'colony' => ['nullable', 'string', 'max:100'], + 'cp' => ['nullable', 'string', 'max:10'], + 'longitude' => ['nullable', 'numeric', 'between:-180,180'], + 'latitude' => ['nullable', 'numeric', 'between:-90,90'], + 'status' => ['nullable', 'boolean'], + ]; + } + + public function messages(): array + { + return [ + 'name.string' => 'El nombre debe ser una cadena de texto', + 'name.max' => 'El nombre no debe superar los 255 caracteres', + 'municipality.string' => 'El municipio debe ser una cadena de texto', + 'municipality.max' => 'El municipio no debe superar los 100 caracteres', + 'address.string' => 'La dirección debe ser una cadena de texto', + 'address.max' => 'La dirección no debe superar los 255 caracteres', + 'colony.string' => 'La colonia debe ser una cadena de texto', + 'colony.max' => 'La colonia no debe superar los 100 caracteres', + 'cp.string' => 'El código postal debe ser una cadena de texto', + 'cp.max' => 'El código postal no debe superar los 10 caracteres', + 'longitude.numeric' => 'La longitud debe ser un número', + 'longitude.between' => 'La longitud debe estar entre -180 y 180', + 'latitude.numeric' => 'La latitud debe ser un número', + 'latitude.between' => 'La latitud debe estar entre -90 y 90', + 'status.boolean' => 'El status debe ser un valor booleano', + ]; + } +} diff --git a/app/Http/Requests/Repuve/VehicleStoreRequest.php b/app/Http/Requests/Repuve/VehicleStoreRequest.php index c023ae4..490be31 100644 --- a/app/Http/Requests/Repuve/VehicleStoreRequest.php +++ b/app/Http/Requests/Repuve/VehicleStoreRequest.php @@ -14,6 +14,7 @@ public function rules(): array { return [ 'folio' => ['required', 'string', 'max:50'], + 'tag_id' => ['required', 'exists:tags,id'], 'files' => ['nullable', 'array', 'min:1'], 'files.*' => ['file', 'mimes:jpeg,png,jpg,pdf', 'max:10240'], 'names' => ['nullable', 'array'], @@ -26,7 +27,8 @@ public function messages(): array return [ 'folio.required' => 'El folio es requerido', 'folio.string' => 'El folio debe ser una cadena de texto', - 'folio.max' => 'El folio no puede exceder 50 caracteres', + 'tag_id.required' => 'El tag_id es requerido', + 'tag_id.exists' => 'El tag_id no existe en el sistema', 'files.array' => 'Los archivos deben ser un array', 'files.*.file' => 'Cada elemento debe ser un archivo válido', 'files.*.mimes' => 'Los archivos deben ser de tipo: jpeg, png, jpg, pdf', diff --git a/app/Http/Requests/Repuve/VehicleUpdateRequest.php b/app/Http/Requests/Repuve/VehicleUpdateRequest.php index 35805b8..0d73552 100644 --- a/app/Http/Requests/Repuve/VehicleUpdateRequest.php +++ b/app/Http/Requests/Repuve/VehicleUpdateRequest.php @@ -13,7 +13,8 @@ public function authorize(): bool public function rules(): array { return [ - 'folio' => ['sometimes', 'string', 'max:50'], + 'folio' => ['required', 'string', 'max:50'], + 'tag_id' => ['required', 'exists:tags,id'], 'files' => ['nullable', 'array', 'min:1'], 'files.*' => ['file', 'mimes:jpeg,png,jpg,pdf', 'max:10240'], 'names' => ['nullable', 'array'], diff --git a/app/Models/Device.php b/app/Models/Device.php index b4180ec..64ba251 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -10,7 +10,6 @@ class Device extends Model use HasFactory; protected $fillable = [ - 'type_id', 'brand', 'serie', 'status', diff --git a/app/Models/Module.php b/app/Models/Module.php index c95dee0..fbd9292 100644 --- a/app/Models/Module.php +++ b/app/Models/Module.php @@ -14,6 +14,7 @@ class Module extends Model 'municipality', 'address', 'colony', + 'cp', 'longitude', 'latitude', 'status', diff --git a/app/Models/Record.php b/app/Models/Record.php index ec20b33..574f21b 100644 --- a/app/Models/Record.php +++ b/app/Models/Record.php @@ -14,6 +14,13 @@ class Record extends Model 'vehicle_id', 'user_id', 'error_id', + 'api_response', + 'error_occurred_at', + ]; + + protected $casts = [ + 'api_response' => 'array', + 'error_occurred_at' => 'datetime', ]; public function vehicle() diff --git a/database/factories/DeviceFactory.php b/database/factories/DeviceFactory.php new file mode 100644 index 0000000..78262ea --- /dev/null +++ b/database/factories/DeviceFactory.php @@ -0,0 +1,66 @@ + + */ +class DeviceFactory extends Factory +{ + protected $model = Device::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $brands = [ + 'estatal', + 'nacional', + ]; + + $year = fake()->numberBetween(2020, 2025); + $randomNumber = fake()->unique()->numerify('######'); + + return [ + 'brand' => fake()->randomElement($brands), + 'serie' => strtoupper(fake()->bothify('??##')) . '-' . $year . '-' . $randomNumber, + 'status' => fake()->boolean(85), // 85% activos + ]; + } + + /** + * Indicate that the device is inactive. + */ + public function inactive(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => false, + ]); + } + + /** + * Indicate that the device is active. + */ + public function active(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => true, + ]); + } + + /** + * Indicate that the device is of a specific brand. + */ + public function brand(string $brand): static + { + return $this->state(fn (array $attributes) => [ + 'brand' => $brand, + ]); + } +} diff --git a/database/factories/ModuleFactory.php b/database/factories/ModuleFactory.php index 6ad75e6..af95d14 100644 --- a/database/factories/ModuleFactory.php +++ b/database/factories/ModuleFactory.php @@ -48,6 +48,7 @@ public function definition(): array 'municipality' => fake()->randomElement($municipalities), 'address' => fake()->streetAddress(), 'colony' => fake()->randomElement($colonies), + 'cp' => fake()->postcode('86###'), 'longitude' => fake()->longitude(-93.5, -92.5), // Tabasco longitude range 'latitude' => fake()->latitude(17.5, 18.5), // Tabasco latitude range 'status' => fake()->boolean(90), // 90% activos diff --git a/database/factories/TagFactory.php b/database/factories/TagFactory.php index ae0f07b..d692547 100644 --- a/database/factories/TagFactory.php +++ b/database/factories/TagFactory.php @@ -72,6 +72,7 @@ public function assigned(): static public function cancelled(): static { return $this->state(fn (array $attributes) => [ + 'vehicle_id' => null, 'status' => 'cancelled', ]); } diff --git a/database/factories/VehicleFactory.php b/database/factories/VehicleFactory.php index aad826a..f27c384 100644 --- a/database/factories/VehicleFactory.php +++ b/database/factories/VehicleFactory.php @@ -4,6 +4,7 @@ use App\Models\Vehicle; use App\Models\Owner; +use App\Models\Tag; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -13,6 +14,21 @@ class VehicleFactory extends Factory { protected $model = Vehicle::class; + /** + * Configure the model factory. + */ + public function configure(): static + { + return $this->afterCreating(function (Vehicle $vehicle) { + // Crear un Tag automáticamente después de crear el vehículo + Tag::factory()->create([ + 'vehicle_id' => $vehicle->id, + 'folio' => $vehicle->folio, + 'status' => 'assigned', + ]); + }); + } + /** * Define the model's default state. * @@ -74,12 +90,12 @@ public function definition(): array } /** - * Generate a realistic VIN (17 characters) + * Generate a realistic VIN */ private function generateVIN(): string { - $wmi = strtoupper(fake()->bothify('???')); // World Manufacturer Identifier - $vds = strtoupper(fake()->bothify('??????')); // Vehicle Descriptor Section + $wmi = strtoupper(fake()->bothify('???')); + $vds = strtoupper(fake()->bothify('??????')); $check = fake()->randomDigit(); $year = strtoupper(fake()->randomLetter()); $plant = fake()->randomDigit(); @@ -125,4 +141,13 @@ public function national(): static 'descripcion_origen' => 'NACIONAL', ]); } + + /** + * Create a vehicle without automatically creating a tag + */ + public function withoutTag(): static + { + return $this->afterCreating(function (Vehicle $vehicle) { + }); + } } diff --git a/database/migrations/2025_10_18_140000_create_modules_table.php b/database/migrations/2025_10_18_140000_create_modules_table.php index b9781f4..b234c79 100644 --- a/database/migrations/2025_10_18_140000_create_modules_table.php +++ b/database/migrations/2025_10_18_140000_create_modules_table.php @@ -17,6 +17,7 @@ public function up(): void $table->string('municipality'); $table->string('address'); $table->string('colony'); + $table->string('cp')->nullable(); $table->decimal('longitude', 10, 8)->nullable(); $table->decimal('latitude', 10, 8)->nullable(); $table->boolean('status')->default(true); diff --git a/database/migrations/2025_10_22_110948_add_error_fields_to_records_table.php b/database/migrations/2025_10_22_110948_add_error_fields_to_records_table.php new file mode 100644 index 0000000..daefe3b --- /dev/null +++ b/database/migrations/2025_10_22_110948_add_error_fields_to_records_table.php @@ -0,0 +1,29 @@ +json('api_response')->nullable()->after('error_id'); + $table->timestamp('error_occurred_at')->nullable()->after('api_response'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('records', function (Blueprint $table) { + $table->dropColumn(['api_response', 'error_occurred_at']); + }); + } +}; diff --git a/database/seeders/DevSeeder.php b/database/seeders/DevSeeder.php index 4377c22..46e13bd 100644 --- a/database/seeders/DevSeeder.php +++ b/database/seeders/DevSeeder.php @@ -38,5 +38,6 @@ public function run(): void // Nivel 4 - Dependen de Nivel 3 $this->call(FileSeeder::class); + $this->call(DeviceSeeder::class); } } diff --git a/database/seeders/DeviceSeeder.php b/database/seeders/DeviceSeeder.php new file mode 100644 index 0000000..e4968b4 --- /dev/null +++ b/database/seeders/DeviceSeeder.php @@ -0,0 +1,69 @@ + 'estatal', + 'serie' => 'ZB01-2024-001234', + 'status' => true, + ], + [ + 'brand' => 'estatal', + 'serie' => 'ZB01-2024-001235', + 'status' => true, + ], + [ + 'brand' => 'estatal', + 'serie' => 'HW02-2024-002456', + 'status' => true, + ], + [ + 'brand' => 'estatal', + 'serie' => 'HW02-2024-002457', + 'status' => true, + ], + [ + 'brand' => 'nacional', + 'serie' => 'DL03-2023-003678', + 'status' => true, + ], + [ + 'brand' => 'nacional', + 'serie' => 'IP04-2024-004890', + 'status' => true, + ], + [ + 'brand' => 'nacional', + 'serie' => 'MT05-2023-005123', + 'status' => true, + ], + [ + 'brand' => 'nacional', + 'serie' => 'TM06-2024-006345', + 'status' => false, // Dispositivo inactivo + ], + ]; + + foreach ($devices as $device) { + Device::create($device); + } + + // Crear dispositivos adicionales con factory para pruebas + Device::factory(10)->create(); + + // Crear algunos dispositivos inactivos adicionales + Device::factory(3)->inactive()->create(); + } +} diff --git a/database/seeders/ModuleSeeder.php b/database/seeders/ModuleSeeder.php index afe106c..7c5e96b 100644 --- a/database/seeders/ModuleSeeder.php +++ b/database/seeders/ModuleSeeder.php @@ -19,6 +19,7 @@ public function run(): void 'municipality' => 'Centro', 'address' => 'Av. Paseo Tabasco 1203', 'colony' => 'Tabasco 2000', + 'cp' => '86000', 'longitude' => -92.9376, 'latitude' => 17.9892, 'status' => true, @@ -28,6 +29,7 @@ public function run(): void 'municipality' => 'Cárdenas', 'address' => 'Calle Benito Juárez No. 305', 'colony' => 'Centro', + 'cp' => '86000', 'longitude' => -93.3808, 'latitude' => 18.0011, 'status' => true, @@ -37,6 +39,7 @@ public function run(): void 'municipality' => 'Comalcalco', 'address' => 'Av. Gregorio Méndez Magaña', 'colony' => 'Centro', + 'cp' => '86000', 'longitude' => -93.2042, 'latitude' => 18.2667, 'status' => true, @@ -46,6 +49,7 @@ public function run(): void 'municipality' => 'Cunduacán', 'address' => 'Calle Carlos Pellicer Cámara', 'colony' => 'Centro', + 'cp' => '86000', 'longitude' => -93.1608, 'latitude' => 18.0667, 'status' => true, @@ -55,6 +59,7 @@ public function run(): void 'municipality' => 'Huimanguillo', 'address' => 'Av. Constitución s/n', 'colony' => 'Centro', + 'cp' => '86000', 'longitude' => -93.3908, 'latitude' => 17.8422, 'status' => true, diff --git a/database/seeders/TagSeeder.php b/database/seeders/TagSeeder.php index a3af2a8..6e39cdb 100644 --- a/database/seeders/TagSeeder.php +++ b/database/seeders/TagSeeder.php @@ -26,8 +26,8 @@ public function run(): void 'lost' => 0, ]; - // Vehículos que ya tienen un tag asignado - $vehiclesWithTag = []; + // Obtener IDs de vehículos que YA tienen un tag asignado (creado por VehicleFactory) + $vehiclesWithTag = Tag::whereNotNull('vehicle_id')->pluck('vehicle_id')->toArray(); // Crear tags para cada paquete foreach ($packages as $package) { diff --git a/routes/api.php b/routes/api.php index 20374ec..b759837 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,8 +1,10 @@ group(function() { // Rutas de inscripción de vehículos - Route::post('inscripcion', [RepuveController::class, 'inscripcionVehiculo']); - Route::post('consulta', [RepuveController::class, 'consultaExpediente']); + Route::post('inscripcion', [InscriptionController::class, 'vehicleInscription']); + Route::post('consulta', [InscriptionController::class, 'searchRecord']); + Route::post('consulta-tag', [InscriptionController::class, 'listTags']); // Rutas de expedientes y documentos Route::get('expediente/{id}/pdf', [RecordController::class, 'generatePdf']); @@ -30,10 +33,16 @@ Route::get('expediente/{id}/pdfConstancia', [RecordController::class, 'generatePdfConstancia']); //Rutas de Actualización - Route::put('expediente/{recordId}', [RepuveController::class, 'actualizarVehiculo']); + Route::put('actualizar-vehiculo', [UpdateController::class, 'vehicleUpdate']); // Rutas de cancelación de constancias Route::post('cancelacion/cancelar', [CancellationController::class, 'cancelarConstancia']); + + //Rutas de Modulos + Route::get('/moduleList', [ModuleController::class, 'index']); + Route::post('/moduleCreate', [ModuleController::class, 'store']); + Route::put('/moduleUpdate/{id}', [ModuleController::class, 'update']); + Route::patch('/moduleStatus/{id}', [ModuleController::class, 'toggleStatus']); }); /** Rutas públicas */