app/models/founder.rb
class Founder < ApplicationRecord
include Concerns::Cacheable
include Concerns::TimeZonable
include Concerns::Eventable
include Concerns::Locationable
include Concerns::Graphable
include Concerns::Socialable
include Concerns::Entityable
extend Concerns::Ignorable
SOCIAL_KEYS = %w(linkedin twitter homepage facebook)
has_and_belongs_to_many :companies, -> { distinct }
has_many :notes
has_many :import_tasks, dependent: :destroy
has_many :emails, dependent: :destroy
has_many :intro_requests, -> { where(pending: false) }, dependent: :destroy
has_many :target_investors, dependent: :destroy
validates :first_name, presence: true
validates :last_name, presence: true
validates :email, uniqueness: { allow_nil: true }
validates :facebook, uniqueness: { allow_nil: true }
validates :twitter, uniqueness: { allow_nil: true }
validates :linkedin, uniqueness: { allow_nil: true }
validates :homepage, uniqueness: { allow_nil: true }
before_validation :normalize_city
before_validation :remove_corporate_homepage
after_commit :start_augment_job, on: :create
after_commit :start_enhance_job, on: :update
action :signed_up, :logged_in, :session_refreshed, :competitor_clicked, :investor_clicked, :investor_targeted
locationable_with :city
devise
def self.active(since = 1.month.ago)
where('logged_in_at > ?', since).joins(:companies).where('companies.primary = ?', true)
end
def self.find_or_create_from_social!(first_name, last_name, social, context: nil)
name_hash = {first_name: first_name, last_name: last_name}
social = social.select { |k,v| v.present? }
attrs = social.merge(name_hash)
if social.blank?
return (context.present? && context.founders.where(name_hash).empty?) ? create!(name_hash) : nil
end
found = social.inject(none) { |scope, (attr, val)| scope.or(where(attr => val)) }.first
found.present? ? found.tap { |f| f.update!(attrs) } : create!(attrs)
end
def self.from_omniauth(auth)
return nil unless auth.present?
first_name, last_name = Util.split_name(auth.info.name)
first_name = auth.info.first_name if auth.info.first_name.present?
last_name = auth.info.last_name if auth.info.last_name.present?
last_name = auth.info.name unless last_name.present?
from_email(auth.info.email, first_name, last_name).tap do |founder|
founder.photo ||= auth.info.image
founder.access_token = auth.credentials.token
founder.refresh_token = auth.credentials.refresh_token if auth.credentials.refresh_token.present?
founder.save!
end
end
def self.from_email(email, first_name = nil, last_name = nil)
retry_invalid do
where(email: email).first_or_create! do |f|
f.first_name = first_name
f.last_name = last_name
end
end
end
def self.export_rating_data(filename)
added = Set.new
CSV.open(filename, 'wb') do |csv|
csv << %w(founder_id investor_id rating)
all
.joins(companies: :investments)
.includes(companies: :investments)
.where.not('investments.investor_id': nil)
.in_batches do |relation|
relation
.joins('LEFT OUTER JOIN news ON (investments.investor_id = news.investor_id AND companies.id = news.company_id)')
.group('founders.id', 'investments.investor_id')
.pluck('founders.id', 'investments.investor_id', '(1 + count(news)) * greatest(1, avg(coalesce(news.sentiment_score, 0) * coalesce(news.sentiment_magnitude, 0) * 10))')
.each do |(founder_id, investor_id, ranking)|
csv << [founder_id, investor_id, ranking]
added.add([founder_id, investor_id])
end
end
scope = all.joins(:target_investors).includes(:target_investors).where.not('target_investors.investor_id': nil)
scope = added.inject(scope) { |s, ids| s.where.not('founders.id = ? AND target_investors.investor_id = ?', *ids) }
scope.in_batches do |relation|
relation
.joins("LEFT OUTER JOIN events ON (founders.id = events.subject_id AND target_investors.investor_id = events.arg1::bigint AND subject_type = 'Founder' AND action = 'investor_clicked')")
.group('founders.id', 'target_investors.investor_id')
.pluck('founders.id', 'target_investors.investor_id', '1 + count(events) * 0.5')
.each do |(founder_id, investor_id, ranking)|
csv << [founder_id, investor_id, ranking]
added.add([founder_id, investor_id])
end
end
scope = all.joins(:intro_requests).includes(:intro_requests)
scope = added.inject(scope) { |s, ids| s.where.not('founders.id = ? AND intro_requests.investor_id = ?', *ids) }
scope.in_batches do |relation|
relation
.joins('LEFT OUTER JOIN emails ON (founders.id = emails.founder_id AND intro_requests.investor_id = emails.investor_id')
.group('founders.id', 'intro_request.investor_id')
.pluck('founders.id', 'intro_request.investor_id', '1 + count(emails) * greatest(1, avg(coalesce(emails.sentiment_score, 0) * coalesce(emails.sentiment_magnitude, 0) * 10))')
.each do |(founder_id, investor_id, ranking)|
csv << [founder_id, investor_id, ranking]
added.add([founder_id, investor_id])
end
end
end
added.count
end
def create_target!(investor)
TargetInvestor.from_investor! self, investor
end
def create_company!(data)
attrs = {
founders: [self],
name: data[:name],
description: data[:description],
location: data[:location].split(',').first,
primary: true,
}
attrs[:industry] = Util.split_slice(data[:industry], Competitor::INDUSTRIES).keys if data[:industry].present?
if data[:domain].present?
Company.where(domain: Util.parse_domain(data[:domain])).first_or_initialize.tap do |c|
c.update! attrs
end
else
Company.create!(attrs)
end.tap do |company|
company.competitions = Company.where(id: data[:companies].split(', ')) if data[:companies].present?
end
end
def name
"#{first_name} #{last_name}"
end
def domain
return nil unless email.present?
Mail::Address.new(email).domain
end
def admin?
email == "#{ENV['ADMIN']}@#{ENV['DOMAIN']}"
end
def drf?
cached { companies.any?(&:funded?) } || domain == ENV['DOMAIN'] || Rails.env.development?
end
def primary_company
@primary_company ||= companies.where(primary: true).last || companies.last
end
def conversations
{
total: target_investors.size,
recents: grouped_conversations,
}
end
def token
update! token: Util.token unless super.present?
super
end
def scanner_enabled?
history_id.present?
end
def scanner_pending?
history_id == 0
end
def as_json(options = {})
super(options.reverse_merge(
only: [:id, :first_name, :last_name, :city, :linkedin, :twitter, :homepage, :unsubscribed, :email, :photo],
methods: [:drf?, :primary_company, :utc_offset, :conversations, :events_with_meta, :stats, :scanner_enabled?]
)).reverse_merge(
target_investors: target_investors.undeleted.includes(:intro_requests).order(stage: :asc, updated_at: :desc).as_json(include: [], methods: [:intro_requests])
)
end
def cached_json
Rails.env.production? ? cache_for_a_hour { as_json } : as_json
end
def existing_target_investor_ids
target_investors.where.not(investor_id: nil).select('investor_id')
end
def ensure_target_investors!
target_investors.create! TargetInvestor::DUMMY_ATTRS if target_investors.count == 0
end
def stats(scope = Email.all)
{
emails: emails.merge(scope).count,
investors: emails.merge(scope).count('DISTINCT investor_id'),
response_time: response_time,
}
end
def events(scope = Event.all)
Event
.where(subject_type: TargetInvestor.name)
.joins('INNER JOIN target_investors ON events.subject_id = target_investors.id')
.where('target_investors.founder_id = ?', id)
.where(action: %w(investor_opened investor_clicked intro_requested investor_replied))
.order(created_at: :desc)
.merge(scope)
end
def events_with_meta
events(Event.limit(3)).select('events.action, events.id, events.arg1, events.arg2, target_investors.first_name, target_investors.last_name, target_investors.firm_name')
end
def save_and_fix_duplicates!
begin
self.save! if self.changed?
rescue ActiveRecord::RecordInvalid => e
raise unless e.record.errors.details.all? { |k,v| v.all? { |e| e[:error].to_sym == :taken } }
attrs = e.record.errors.details.transform_values { |v| v.first[:value] }
other = Founder.where(attrs).first
raise unless other.present?
raise if other.logged_in_at.present?
self.class.migrate_other(self, other)
end
end
def self.migrate_other(founder, other)
other.companies.find_each do |company|
founder.companies << company unless founder.companies.include?(company)
end
other.entities.find_each do |entity|
founder.entities << entity unless founder.entities.include?(entity)
end
other.destroy!
founder.save!
end
def graph_node
@graph_node ||= super || begin
address = Mail::Address.new("\"#{name}\" <#{first_name.downcase}@#{primary_company.domain}>") rescue nil
Graph.get(address) if address.present?
end
end
private
def set_metrics!
return unless crunchbase_person.present?
self.affiliated_exits = 0
companies = Set.new
(crunchbase_person.jobs + crunchbase_person.advisory_roles).each do |job|
company = Company.from_crunchbase_id(job.organization.permalink)
next unless company.present?
unless (company.ipo_date || company.acquisition_date).present?
company.send(:set_capital_fields!)
begin
company.save!
rescue ActiveRecord::RecordInvalid
next
end
end
next unless (date = company.ipo_date || company.acquisition_date).present?
next if job.started_on.present? && Date.parse(job.started_on) > date
unless companies.include?(job.organization.permalink)
companies.add job.organization.permalink
self.affiliated_exits += 1
end
end
end
def crunchbase_person
@crunchbase_person ||= begin
person = Http::Crunchbase::Person.new(cb_id)
person if person.found?
end if cb_id.present?
end
def normalize_city
self.city = Util.normalize_city(self.city) if self.city.present?
end
def remove_corporate_homepage
return unless self.homepage.present?
domain = Util.parse_domain(self.homepage)
if Company.where(domain: domain).count > 0 || Competitor.where(domain: domain).count > 0
self.homepage = nil
end
end
def set_response_time!
update! response_time: Util.average_response_time(emails, :investor_id)
end
def grouped_conversations
target_investors
.order(created_at: :desc)
.pluck(:stage, :firm_name)
.group_by { |v| TargetInvestor::CATEGORIES[v.first] }
.transform_values { |v| v.map(&:last) }
end
def start_augment_job
FounderEnhanceJob.perform_later(self.id, augment: email.present?)
end
def start_enhance_job
return unless logged_in_at_changed?
IntroRequest.where(founder: self).update_all preview_html: nil
FounderEnhanceJob.perform_later(self.id, augment: false) if ip_address_changed?
end
end