Commit iniziale di eminuxCRM: CRM Rails con pipeline, campagne email e Docker.
Include autenticazione, progetti isolati, mail marketing HTML con SMTP, test A/B e editor WYSIWYG. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
require "csv"
|
||||
|
||||
class CampaignImport::MatchlivetvLaunch
|
||||
CAMPAIGN_NAME = "Campagna lancio 01".freeze
|
||||
SPORT = "Pallavolo".freeze
|
||||
|
||||
Result = Struct.new(:wiped_organizations, :imported, :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)
|
||||
@path = Pathname.new(path)
|
||||
@user = user
|
||||
@project = project
|
||||
@wipe = wipe
|
||||
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
|
||||
imported = 0
|
||||
errors = []
|
||||
|
||||
rows.each_with_index do |row, idx|
|
||||
import_row!(row)
|
||||
imported += 1
|
||||
rescue StandardError => e
|
||||
errors << { line: idx + 2, name: row["Società"], message: e.message }
|
||||
end
|
||||
|
||||
Result.new(wiped_organizations: wiped, imported: imported, 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: cell(row, "Email verificata")&.downcase,
|
||||
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: 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 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
|
||||
Reference in New Issue
Block a user