require "csv" class CampaignImport::MatchlivetvLaunch CAMPAIGN_NAME = "Campagna lancio 01".freeze SPORT = "Pallavolo".freeze LEGAL_PREFIXES = /\b(ssdarl|ssd arl|a\.?s\.?d\.?|asd|s\.?s\.?d\.?|ssd|s\.?s\.?|a\.?f\.?|a\.?p\.?|c\.?s\.?|polisportiva)\b/i Result = Struct.new(:wiped_organizations, :imported, :skipped, :errors, keyword_init: true) STREAMING_MAP = { "NON RILEVATO" => "not_detected", "LIMITATO" => "limited", "SI / PARZIALE" => "yes_partial", "SI" => "yes", "SI - SPORTCAM" => "yes_sportcam", "SI SPORTCAM" => "yes_sportcam" }.freeze GENDER_MAP = { "F" => "female", "M" => "male", "M/F" => "mixed" }.freeze SEND_MAP = { "DA INVIARE" => "to_send", "INVIATO" => "sent", "NON INVIARE" => "no_send" }.freeze def initialize(path:, user: nil, project: nil, wipe: true, sport: SPORT, campaign_name: CAMPAIGN_NAME) @path = Pathname.new(path) @user = user @project = project @wipe = wipe @sport = sport @campaign_name = campaign_name end def call raise ArgumentError, "File non trovato: #{@path}" unless @path.exist? @project ||= Project.find_by!(code: "matchlivetv") @user ||= User.find_by!(role: "admin") Current.user = @user Current.project = @project wiped = @wipe ? wipe_project! : 0 index_existing_names! imported = 0 skipped = 0 errors = [] rows.each_with_index do |row, idx| name = cell(row, "Società") if duplicate_name?(name) skipped += 1 next end import_row!(row) remember_name!(name) imported += 1 rescue StandardError => e message = e.is_a?(ActiveRecord::RecordInvalid) ? e.record.errors.full_messages.to_sentence : e.message errors << { line: idx + 2, name: row["Società"], message: message } end Result.new(wiped_organizations: wiped, imported: imported, skipped: skipped, errors: errors) ensure Current.user = nil Current.project = nil end private def rows CSV.read(@path, headers: true, encoding: "bom|utf-8") end def wipe_project! destroyed = 0 Organization.transaction do Opportunity.where(project_id: @project.id).find_each(&:destroy!) org_ids = OrganizationProject.where(project_id: @project.id).pluck(:organization_id) OrganizationProject.where(project_id: @project.id).delete_all Organization.where(id: org_ids).find_each do |org| next if org.organization_projects.exists? org.destroy! destroyed += 1 end end destroyed end def import_row!(row) name = cell(row, "Società") raise "Società vuota" if name.blank? org = Organization.new( name: name, organization_type: "societa_sportiva", sport: @sport, country: "Italia", region: cell(row, "Regione"), province: cell(row, "Prov."), email: normalize_email(cell(row, "Email verificata")), website: cell(row, "Sito / profilo"), source_url: cell(row, "Fonte contatto / ricerca"), commercial_fit: cell(row, "Evidenza / fit commerciale"), team_gender: GENDER_MAP.fetch(normalize_key(cell(row, "M/F"))) { raise "M/F sconosciuto: #{row['M/F']}" }, streaming_status: streaming_status_for(cell(row, "Streaming rilevato")), list_position: integer_cell(row, "N."), verified_at: parse_date(cell(row, "Data verifica")), status: yes?(cell(row, "Conversione")) ? "active_customer" : "prospect", lead_source: "campaign", assigned_user: @user, notes: cell(row, "Note follow-up") ) org.projects = [@project] org.save! first_name, last_name, role = contact_from(org.email, org.name) org.contacts.create!( first_name: first_name, last_name: last_name, role: role, email: org.email, preferred_contact_method: "email", primary_contact: true ) converted = yes?(cell(row, "Conversione")) demo = yes?(cell(row, "Demo / Trial")) stage = if converted "won" elsif demo "demo_trial" else "to_contact" end org.opportunities.create!( name: @campaign_name, project: @project, pipeline_stage: stage, product: blank_if_no(cell(row, "Piano")), assigned_user: @user, ab_variant: cell(row, "Test A/B").to_s.upcase.presence, send_status: SEND_MAP.fetch(normalize_key(cell(row, "Stato invio")), "to_send"), sent_on: parse_date(cell(row, "Data invio")), outcome: cell(row, "Esito"), demo_trial: demo, converted: converted, notes: [cell(row, "Evidenza / fit commerciale"), cell(row, "Note follow-up")].compact_blank.join("\n\n") ) end def index_existing_names! @existing_names = {} Organization.find_each { |org| remember_name!(org.name) } end def remember_name!(name) key = normalize_org_name(name) @existing_names[key] = true if key.present? end def duplicate_name?(name) key = normalize_org_name(name) key.blank? || @existing_names[key] end def normalize_org_name(name) I18n.transliterate(name.to_s) .downcase .gsub("'", " ") .gsub(LEGAL_PREFIXES, " ") .gsub(/[^a-z0-9]+/, " ") .squeeze(" ") .strip end def blank_if_no(value) return if value.blank? return if %w[NO N/A].include?(normalize_key(value)) value end def normalize_email(value) email = value.to_s.strip.downcase.gsub(/\s+/, "") email.presence end def cell(row, header) value = row[header].to_s.strip value.presence end def integer_cell(row, header) raw = cell(row, header) return if raw.blank? raw.to_i end def normalize_key(value) I18n.transliterate(value.to_s) .gsub(/[\u2013\u2014\u2212]/, "-") .encode("ASCII", invalid: :replace, undef: :replace, replace: "") .strip .upcase .gsub(/\s+/, " ") end def streaming_status_for(value) key = normalize_key(value) return STREAMING_MAP[key] if STREAMING_MAP.key?(key) return "yes_sportcam" if key.include?("SPORTCAM") return "yes_partial" if key.include?("PARZIALE") return "not_detected" if key.include?("NON RILEVATO") return "limited" if key.include?("LIMITATO") return "yes" if key == "SI" || key.start_with?("SI ") raise "Streaming sconosciuto: #{value}" end def yes?(value) %w[SI YES TRUE 1].include?(normalize_key(value)) end def parse_date(value) return if value.blank? return Date.iso8601(value) if value.match?(/\A\d{4}-\d{2}-\d{2}\z/) return Date.strptime(value, "%d/%m/%Y") if value.match?(/\A\d{1,2}\/\d{1,2}\/\d{4}\z/) serial = Float(value) Date.new(1899, 12, 30) + serial.to_i rescue ArgumentError, TypeError nil end def contact_from(email, org_name) local = email.to_s.split("@").first.to_s if local.match?(/\A[a-z]+[._][a-z]+\z/i) first, last = local.split(/[._]/) [first.capitalize, last.capitalize, "Contatto"] elsif local.match?(/\A[a-z]\.[a-z]+\z/i) initial, last = local.split(".") ["#{initial.upcase}.", last.capitalize, "Contatto"] else token = org_name.to_s.split(/[\s\/–-]+/).last.presence || "Società" ["Segreteria", token, "Segreteria"] end end end