require "csv" class CsvImport::Organizations STANDARD_HEADERS = %w[ organization_name organization_type sport country region province city website organization_email contact_first_name contact_last_name contact_role contact_email contact_phone lead_source notes ].freeze Result = Struct.new(:imported, :skipped, :errors, keyword_init: true) def initialize(file:, user:, mapping: nil, project: nil) @file = file @user = user @mapping = mapping @project = project || Current.project end def preview(limit: 10) rows = [] CSV.foreach(@file.path, headers: true, encoding: "bom|utf-8") do |row| rows << row.to_h break if rows.size >= limit end { headers: rows.first&.keys || [], rows: rows } end def import! imported = 0 skipped = 0 errors = [] CSV.foreach(@file.path, headers: true, encoding: "bom|utf-8").with_index(2) do |row, line| Current.user = @user attrs = mapped_attrs(row) if duplicate?(attrs) skipped += 1 next end Organization.transaction do org = Organization.new( name: attrs[:organization_name], organization_type: normalize_type(attrs[:organization_type]), sport: attrs[:sport], country: attrs[:country].presence || "Italia", region: attrs[:region], province: attrs[:province], city: attrs[:city], website: normalize_website(attrs[:website]), email: attrs[:organization_email], lead_source: normalize_lead_source(attrs[:lead_source]), notes: attrs[:notes], status: "prospect", assigned_user: @user ) org.projects = [@project].compact org.save! if attrs[:contact_first_name].present? || attrs[:contact_last_name].present? org.contacts.create!( first_name: attrs[:contact_first_name].presence || "N/D", last_name: attrs[:contact_last_name].presence || "N/D", role: attrs[:contact_role], email: attrs[:contact_email], phone: attrs[:contact_phone], primary_contact: true ) end end imported += 1 rescue StandardError => e errors << { line: line, message: e.message, row: row.to_h } end Result.new(imported: imported, skipped: skipped, errors: errors) ensure Current.user = nil end private def mapped_attrs(row) source = row.to_h.transform_keys { |k| k.to_s.strip } STANDARD_HEADERS.index_with do |header| key = @mapping&.dig(header) || header source[key].to_s.strip.presence end.symbolize_keys end def duplicate?(attrs) name = attrs[:organization_name].to_s email = attrs[:organization_email].to_s.downcase website = normalize_website(attrs[:website]) return true if name.present? && Organization.where("LOWER(name) = ?", name.downcase).exists? return true if email.present? && Organization.where("LOWER(email) = ?", email).exists? return true if website.present? && Organization.where("LOWER(website) = ?", website.downcase).exists? false end def normalize_website(value) return if value.blank? value.to_s.strip.downcase.sub(%r{\Ahttps?://}, "").sub(%r{/\z}, "") end def normalize_type(value) return "altro" if value.blank? key = value.to_s.downcase.gsub(/\s+/, "_") return key if Catalog::ORGANIZATION_TYPES.key?(key) Catalog::ORGANIZATION_TYPES.find { |_k, label| label.downcase == value.to_s.downcase }&.first || "altro" end def normalize_lead_source(value) return if value.blank? key = value.to_s.downcase.gsub(/\s+/, "_") return key if Catalog::LEAD_SOURCES.key?(key) Catalog::LEAD_SOURCES.find { |_k, label| label.downcase == value.to_s.downcase }&.first || "other" end end