require 'zip' require 'stringio' class FileProcessorJob include Sidekiq::Job def perform(inn, report_id) raise ArgumentError, "Некорректный ИНН: #{inn}" unless inn.to_s.match?(/\A\d{10}(\d{2})?\z/) report = Report.find(report_id) Rails.logger.info "=== Начало загрузки для ИНН: #{inn} ===" temp_dir = Rails.root.join('tmp', 'reports', inn.to_s.gsub(/[^0-9]/, '')) FileUtils.mkdir_p(temp_dir) download_inn(inn, temp_dir.to_s) # Найти или создать организацию по ИНН organization = Organization.find_or_create_by!(inn: inn) Dir.glob("#{temp_dir}/**/*").select { |f| File.file?(f) }.each do |file_path| # Прикрепляем файл к Report (обратная совместимость) report.files.attach( io: File.open(file_path), filename: File.basename(file_path) ) # Создаём YearlyFile и прикрепляем xlsx year_from_filename = File.basename(file_path, '.*').split('_')[2].to_i year_from_filename = 0 unless (2000..2099).cover?(year_from_filename) yearly_file = organization.yearly_files.build(year: year_from_filename) yearly_file.file.attach( io: File.open(file_path), filename: File.basename(file_path) ) yearly_file.save! # Парсим данные из xlsx ReportParser.new(yearly_file).call end # Удали временные файлы FileUtils.rm_rf(temp_dir) report.update!(status: 'completed') Rails.logger.info "=== Завершено для ИНН: #{inn} ===" rescue => e report.update!(status: 'failed') if report Rails.logger.error "=== ОШИБКА: #{e.message} ===" raise end private def get_content(url, json_need = false) headers = { 'User-Agent' => "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", 'Accept-Language' => 'en-US,en;q=0.9', 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' } response = HTTParty.get(url, headers: headers, timeout: 30) raise "HTTP #{response.code} при запросе #{url}" unless response.success? json_need ? JSON.parse(response.body) : response.body end def download_inn(inn, output) url = "https://bo.nalog.gov.ru/advanced-search/organizations/search?query=#{inn}&page=0&size=20" content = get_content(url, true) org_entry = content.dig('content', 0) or raise "Организация с ИНН #{inn} не найдена" id = org_entry['id'] url = "https://bo.nalog.gov.ru/nbo/organizations/#{id}/bfo/" content = get_content(url, true) raise "Нет отчётности для организации #{id}" if content.blank? periods_and_details_ids = content.filter_map do |item| details_id = item.dig('typeCorrections', 0, 'correction', 'id') next if details_id.nil? [item['period'], details_id] end periods_and_details_ids.each do |period, details_id| url = "https://bo.nalog.gov.ru/download/bfo/#{id}?auditReport=false&balance=true"\ "&capitalChange=true&clarification=false&targetedFundsUsing=false&detailsId=#{details_id}"\ "&financialResult=true&fundsMovement=true&type=XLS&period=#{period}" content = get_content(url) zip_file = Zip::File.open_buffer(StringIO.new(content)) zip_file.each do |entry| output_file = File.join(output, entry.name) FileUtils.mkdir_p(File.dirname(output_file)) zip_file.extract(entry, output_file) { true } # перезаписывать если существует end end end end