repuveService = $repuveService; $this->padronEstatalService = $padronEstatalService; } /* * Inscripción de vehículo al REPUVE */ public function vehicleInscription(VehicleStoreRequest $request) { try { $folio = $request->input('folio'); $tagNumber = $request->input('tag_number'); $placa = $request->input('placa'); $telefono = $request->input('telefono'); // Buscar Tag y validar que NO tenga vehículo asignado $tag = Tag::where('folio', $folio)->first(); if (!$tag) { return ApiResponse::NOT_FOUND->response([ 'message' => 'No se encontró el tag con el folio y tag_number proporcionados.', 'folio' => $folio, 'tag_number' => $tagNumber, ]); } if (!$tag->isAvailable()) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'El tag ya está asignado a un vehículo. Use actualizar en su lugar.', 'current_status' => $tag->status->name, ]); } // Iniciar transacción DB::beginTransaction(); if (!$tag->tag_number) { $existingTag = Tag::where('tag_number', $tagNumber)->first(); if ($existingTag && $existingTag->id !== $tag->id) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El tag_number ya está asignado a otro folio.', 'tag_number' => $tagNumber, 'folio_existente' => $existingTag->folio, ]); } // Guardar tag_number $tag->tag_number = $tagNumber; $tag->save(); } elseif ($tag->tag_number !== $tagNumber) { // Si el tag ya tiene un tag_number diferente, validar DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El folio ya tiene un tag_number diferente asignado.', 'folio' => $folio, 'tag_number_actual' => $tag->tag_number, 'tag_number_enviado' => $tagNumber, ]); } // Obtener datos del servicio ESTATAL $datosCompletosRaw = $this->padronEstatalService->getVehiculoByPlaca($placa); // Extraer datos del servicio estatal $vehicleDataEstatal = $this->padronEstatalService->extraerDatosVehiculo($datosCompletosRaw); $ownerData = $this->padronEstatalService->extraerDatosPropietario($datosCompletosRaw); // Obtener NIV para consultar REPUVE Nacional $niv = $vehicleDataEstatal['niv']; if (empty($niv)) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El padrón estatal no retornó un NIV válido para la placa proporcionada.', 'placa' => $placa, ]); } // Consultar REPUVE Nacional para corroborar el vehículo y obtener folio_CI $repuveNacionalData = $this->repuveService->consultarVehiculo($niv, $placa); // Verificar si hubo error en la consulta a REPUVE Nacional if ($repuveNacionalData['has_error'] ?? false) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al consultar REPUVE Nacional.', 'error' => $repuveNacionalData['error_message'] ?? 'Error desconocido', ]); } // Determinar si es inscripción primera vez o sustitución // Si el folio de la constancia viene vacío, es primera vez; si no, es sustitución $folioRepuve = $repuveNacionalData['folio_CI'] ?? null; $actionType = empty($folioRepuve) ? 'sustitucion_primera_vez' : 'sustitucion'; // Verificar robo $roboResult = $this->checkIfStolen($niv, $placa); // Solo bloquear si está marcado como robado if ($roboResult['is_robado'] ?? false) { DB::rollBack(); return ApiResponse::FORBIDDEN->response([ 'message' => '¡El vehículo presenta reporte de robo! No se puede continuar con la inscripción.', 'niv' => $niv, 'placa' => $placa, ]); } // Crear propietario $owner = Owner::updateOrCreate( ['rfc' => $ownerData['rfc']], [ 'name' => $ownerData['name'], 'paternal' => $ownerData['paternal'], 'maternal' => $ownerData['maternal'], 'curp' => $ownerData['curp'], 'address' => $ownerData['address'], 'tipopers' => $ownerData['tipopers'], 'pasaporte' => $ownerData['pasaporte'], 'licencia' => $ownerData['licencia'], 'ent_fed' => $ownerData['ent_fed'], 'munic' => $ownerData['munic'], 'callep' => $ownerData['callep'], 'num_ext' => $ownerData['num_ext'], 'num_int' => $ownerData['num_int'], 'colonia' => $ownerData['colonia'], 'cp' => $ownerData['cp'], 'telefono' => $telefono ] ); // Crear vehículo con datos del Padrón Estatal (fuente primaria) $vehicle = Vehicle::create(array_merge( $vehicleDataEstatal, ['owner_id' => $owner->id] )); // Asignar Tag al vehículo $tag->markAsAssigned($vehicle->id, $folio); VehicleTagLog::create([ 'vehicle_id' => $vehicle->id, 'tag_id' => $tag->id, 'action_type' => $actionType, 'performed_by' => Auth::id(), ]); // Crear registro $record = Record::create([ 'folio' => $folio, 'vehicle_id' => $vehicle->id, 'user_id' => Auth::id(), 'module_id' => Auth::user()->module_id, ]); // Procesar archivos $uploadedFiles = []; if ($request->hasFile('files')) { $files = $request->file('files'); $nameIds = $request->input('name_id', []); if (!empty($nameIds)) { $validIds = CatalogNameImg::whereIn('id', $nameIds)->pluck('id')->toArray(); if (count($validIds) !== count($nameIds)) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Algunos IDs del catálogo de nombres no son válidos', 'provided_id' => $nameIds, 'valid_id' => $validIds, ]); } } foreach ($files as $index => $file) { // Obtener el name_id del request o usar null como fallback $nameId = $nameIds[$index] ?? null; if ($nameId === null) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => "Falta el name_id para el archivo en el índice {$index}", 'file_index' => $index, ]); } // Obtener el nombre del catálogo para el nombre del archivo $catalogName = CatalogNameImg::find($nameId); $extension = $file->getClientOriginalExtension(); $fileName = $catalogName->name . '_' . date('dmY_His') . '.' . $extension; $path = $file->storeAs("records/{$record->folio}", $fileName, 'public'); $md5 = md5_file($file->getRealPath()); $fileRecord = File::create([ 'name_id' => $nameId, 'path' => $path, 'md5' => $md5, 'record_id' => $record->id, ]); $uploadedFiles[] = [ 'id' => $fileRecord->id, 'name' => $catalogName->name, 'path' => $fileRecord->path, 'url' => $fileRecord->url, ]; } } // Agregar datos de la constancia de inscripción $datosCompletosRaw['folio_CI'] = $folio; $datosCompletosRaw['identificador_CI'] = $tagNumber; ProcessRepuveResponse::dispatch($record->id, $datosCompletosRaw); DB::commit(); $record->load(['vehicle.owner', 'vehicle.tag', 'files', 'user']); return ApiResponse::CREATED->response([ 'success' => true, 'message' => 'Vehículo y propietario guardados 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' => $record->vehicle->id, 'placa' => $record->vehicle->placa, 'niv' => $record->vehicle->niv, 'marca' => $record->vehicle->marca, 'linea' => $record->vehicle->linea, 'modelo' => $record->vehicle->modelo, 'color' => $record->vehicle->color, ], 'owner' => [ 'id' => $record->vehicle->owner->id, 'full_name' => $record->vehicle->owner->full_name, 'rfc' => $record->vehicle->owner->rfc, ], 'tag' => [ 'id' => $record->vehicle->tag->id, 'folio' => $record->vehicle->tag->folio, 'tag_number' => $record->vehicle->tag->tag_number, 'status' => $record->vehicle->tag->status->name, ], 'files' => $uploadedFiles, 'total_files' => count($uploadedFiles), ]); } catch (PadronEstatalException $e) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al consultar el padrón estatal.', 'error' => $e->getMessage(), ]); } catch (\Exception $e) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'Error al procesar la inscripción del vehículo', 'error' => $e->getMessage(), ]); } } private function checkIfStolen(?string $niv = null, ?string $placa = null) { return $this->repuveService->verificarRobo($niv, $placa); } public function searchRecord(Request $request) { $request->validate([ 'folio' => 'nullable|string', 'placa' => 'nullable|string', 'vin' => 'nullable|string', 'tag_number' => 'nullable|string', 'module_id' => 'nullable|integer|exists:modules,id', 'action_type' => 'nullable|string|in:sustitucion_primera_vez,actualizacion,sustitucion,cancelacion', 'status' => 'nullable|string', 'start_date' => 'nullable|date', 'end_date' => 'nullable|date|after_or_equal:start_date', ], [ 'folio.required_without_all' => 'Se requiere al menos un criterio de búsqueda.', 'placa.required_without_all' => 'Se requiere al menos un criterio de búsqueda.', 'vin.required_without_all' => 'Se requiere al menos un criterio de búsqueda.', 'start_date.date' => 'La fecha de inicio debe ser una fecha válida.', 'end_date.date' => 'La fecha de fin debe ser una fecha válida.', 'end_date.after_or_equal' => 'La fecha de fin debe ser posterior o igual a la fecha de inicio.', ]); $records = Record::forUserModule(Auth::user()) ->with([ // Vehículo y propietario 'vehicle', 'vehicle.owner', // Tag con Package 'vehicle.tag:id,vehicle_id,folio,tag_number,status_id,package_id', 'vehicle.tag.status:id,code,name', 'vehicle.tag.package:id,lot,box_number', // Archivos 'files:id,record_id,name_id,path,md5', 'files.catalogName:id,name', // Operador y módulo 'user:id,name,username,module_id', 'module:id,name', // Error si existe 'error:id,code,description', // Log de acciones 'vehicle.vehicleTagLogs' => function ($q) { $q->with([ 'tag:id,folio,tag_number,status_id,module_id,package_id', 'tag.status:id,code,name', 'tag.module:id,name', 'tag.package:id,lot,box_number', 'performedBy:id,name', ])->orderBy('created_at', 'DESC'); }, ])->orderBy('id', 'ASC'); if ($request->filled('folio')) { $records->whereHas('vehicle.tag', function ($q) use ($request) { $q->where('folio', 'LIKE', '%' . $request->input('folio') . '%'); }); } if ($request->filled('placa')) { $records->whereHas('vehicle', function ($q) use ($request) { $q->where('placa', 'LIKE', '%' . $request->input('placa') . '%'); }); } if ($request->filled('vin')) { $records->whereHas('vehicle', function ($q) use ($request) { $q->where('niv', 'LIKE', '%' . $request->input('vin') . '%'); }); } if ($request->filled('tag_number')) { $records->whereHas('vehicle.tag', function ($q) use ($request) { $q->where('tag_number', 'LIKE', '%' . $request->input('tag_number') . '%'); }); } // Filtro por módulo if ($request->filled('module_id')) { $records->where('module_id', $request->input('module_id')); } // Filtro por tipo de acción if ($request->filled('action_type')) { $records->whereHas('vehicle.vehicleTagLogs', function ($q) use ($request) { $q->where('action_type', $request->input('action_type')) ->whereRaw('id = ( SELECT MAX(id) FROM vehicle_tags_logs WHERE vehicle_id = vehicle.id )'); }); } // Filtro por status del tag if ($request->filled('status')) { $records->whereHas('vehicle.tag.status', function ($q) use ($request) { $q->where('code', $request->input('status')); }); } // Filtro por rango de fechas if ($request->filled('start_date')) { $records->whereDate('created_at', '>=', $request->input('start_date')); } if ($request->filled('end_date')) { $records->whereDate('created_at', '<=', $request->input('end_date')); } // Paginación $paginatedRecords = $records->paginate(config('app.pagination')); if ($paginatedRecords->isEmpty()) { return ApiResponse::NOT_FOUND->response([ 'message' => 'No se encontraron registros con los criterios de búsqueda proporcionados.', ]); } // Transformación de datos $paginatedRecords->getCollection()->transform(function ($record) { $vehicleLogs = $record->vehicle->vehicleTagLogs->sortBy('created_at'); $firstLog = $vehicleLogs->first(); // primer evento // Historial: todos los logs excepto el primero, uno por evento $tagsHistory = []; foreach ($vehicleLogs->skip(1)->values() as $index => $log) { $tag = $log->tag; $tagsHistory[] = [ 'order' => $index + 1, 'log_id' => $log->id, 'tag_id' => $log->tag_id, 'action_type' => $log->action_type, 'folio' => $tag?->folio, 'tag_number' => $tag?->tag_number, 'box_number' => $tag?->package?->box_number, 'status' => $tag?->status?->code ?? 'unknown', 'module' => $tag?->module ? ['id' => $tag->module->id, 'name' => $tag->module->name] : null, 'operator' => $log->performedBy ? ['id' => $log->performedBy->id, 'name' => $log->performedBy->name] : null, 'performed_at' => $log->created_at, 'cancelled_at' => $log->cancellation_at, 'is_current' => $tag?->id === $record->vehicle->tag?->id, ]; } return [ 'id' => $record->id, 'folio' => $record->folio, 'created_at' => $record->created_at, // TIPO DE TRÁMITE (siempre el primer evento) 'action_type' => $firstLog?->action_type, 'action_date' => $firstLog?->created_at ?? $record->created_at, // HISTORIAL DE TRÁMITES 'tags_history' => $tagsHistory, 'total_tags' => count($tagsHistory), // MÓDULO 'module' => $record->module ? [ 'id' => $record->module->id, 'name' => $record->module->name, ] : null, // OPERADOR 'operator' => $record->user ? [ 'id' => $record->user->id, 'name' => $record->user->name, 'username' => $record->user->username, ] : null, // VEHÍCULO 'vehicle' => [ 'id' => $record->vehicle->id, 'placa' => $record->vehicle->placa, 'niv' => $record->vehicle->niv, 'marca' => $record->vehicle->marca, 'linea' => $record->vehicle->linea, 'sublinea' => $record->vehicle->sublinea, 'modelo' => $record->vehicle->modelo, 'color' => $record->vehicle->color, 'numero_motor' => $record->vehicle->numero_motor, 'clase_veh' => $record->vehicle->clase_veh, 'tipo_servicio' => $record->vehicle->tipo_servicio, 'rfv' => $record->vehicle->rfv, 'nrpv' => $record->vehicle->nrpv, 'reporte_robo' => $record->vehicle->reporte_robo, // PROPIETARIO 'owner' => $record->vehicle->owner ? [ 'id' => $record->vehicle->owner->id, 'name' => $record->vehicle->owner->name, 'paternal' => $record->vehicle->owner->paternal, 'maternal' => $record->vehicle->owner->maternal, 'full_name' => $record->vehicle->owner->full_name, 'rfc' => $record->vehicle->owner->rfc, 'curp' => $record->vehicle->owner->curp, 'telefono' => $record->vehicle->owner->telefono, 'address' => $record->vehicle->owner->address, ] : null, // TAG ACTUAL 'tag' => $record->vehicle->tag ? [ 'id' => $record->vehicle->tag->id, 'folio' => $record->vehicle->tag->folio, 'tag_number' => $record->vehicle->tag->tag_number, 'status' => $record->vehicle->tag->status ? [ 'id' => $record->vehicle->tag->status->id, 'code' => $record->vehicle->tag->status->code, 'name' => $record->vehicle->tag->status->name, ] : null, 'package' => $record->vehicle->tag->package ? [ 'id' => $record->vehicle->tag->package->id, 'lot' => $record->vehicle->tag->package->lot, 'box_number' => $record->vehicle->tag->package->box_number, ] : null, ] : null, ], // Archivos 'files' => $record->files->map(function ($file) { return [ 'id' => $file->id, 'name_id' => $file->name_id, 'name' => $file->catalogName?->name, 'path' => $file->path, 'url' => $file->url, ]; }), // Error 'error' => $record->error ? [ 'id' => $record->error->id, 'code' => $record->error->code, 'description' => $record->error->description, ] : null, // Respuesta de REPUVE 'api_response' => $record->api_response, ]; }); return ApiResponse::OK->response([ 'records' => $paginatedRecords ]); } public function stolen(Request $request) { $request->validate([ 'vin' => 'nullable|string|min:17|max:17', 'placa' => 'nullable|string', ], [ 'vin.required_without' => 'Debe proporcionar al menos VIN o PLACA.', 'placa.required_without' => 'Debe proporcionar al menos VIN o PLACA.', ]); // Validar que al menos uno esté presente if (!$request->filled('vin') && !$request->filled('placa')) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'Debe proporcionar al menos VIN o PLACA.', ]); } try { $vin = $request->input('vin'); $placa = $request->input('placa'); // Verificar robo usando el servicio $resultado = $this->repuveService->verificarRobo($vin, $placa); $isStolen = $resultado['is_robado'] ?? false; $vehicle = Vehicle::where(function ($query) use ($vin, $placa) { if ($vin) { $query->orWhere('niv', $vin); } if ($placa) { $query->orWhere('placa', $placa); } })->first(); $actualizar = false; if ($vehicle) { $vehicle->reporte_robo = $isStolen; $vehicle->save(); $actualizar = true; } return ApiResponse::OK->response([ 'vin' => $vin ?: null, 'placa' => $placa ?: null, 'robado' => $isStolen, 'estatus' => $isStolen ? 'REPORTADO COMO ROBADO' : 'SIN REPORTE DE ROBO', 'message' => $isStolen ? 'El vehículo tiene reporte de robo en REPUVE.' : 'El vehículo no tiene reporte de robo.', 'fecha' => now()->toDateTimeString(), 'detalle_robo' => $resultado, 'existe_registro_BD' => $vehicle ? true : false, 'actualizado_reporte_robo' => $actualizar, ]); } catch (\Exception $e) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al consultar el estado de robo del vehículo.', 'error' => $e->getMessage(), ]); } } }