app/models/company.rb

Summary

Maintainability
D
1 day
Test Coverage
class Company < ActiveRecord::Base
  include Concerns::Cacheable
  include ActionView::Helpers::NumberHelper
  include Concerns::AttributeArrayable
  include Concerns::Domainable

  has_one :tweeter, as: :owner, dependent: :destroy
  has_many :pitches, -> { order(when: :desc ) }
  has_many :news, dependent: :destroy
  has_many :cards, -> { order(updated_at: :desc ).where(archived: false) }
  has_many :calendar_events
  has_many :investments, dependent: :destroy
  belongs_to :team
  has_and_belongs_to_many :users, -> { distinct }
  has_many :competitors, through: :investments
  has_and_belongs_to_many :founders, -> { distinct }

  validates :name, presence: true

  validates :domain, uniqueness: { allow_nil: true }
  validates :crunchbase_id, uniqueness: { allow_nil: true }
  validates :al_id, uniqueness: { allow_nil: true }
  validates :capital_raised, presence: true, numericality: { only_integer: true }

  array :industry

  scope :pitched, -> { joins(:pitches) }
  scope :decided, -> { pitched.where('pitches.decision IS NOT NULL') }
  scope :undecided, -> { pitched.where('pitches.decision IS NULL') }
  scope :portfolio, -> { pitched.where('pitches.funded': true) }

  after_create :add_to_wit!
  after_commit :start_relationships_job, on: :create
  before_validation :normalize_location

  def pitch
    @pitch ||= pitches.last
  end

  def card
    @card ||= cards.last
  end

  def in_list?(list)
    card.present? ? card.list.in?(Array.wrap(list)) : false
  end

  def passed?
    team.present? && in_list?([team.lists.rejected, team.lists.passed])
  end

  def funded?
    pitch&.funded? || (team.present? && in_list?(team.funded_lists))
  end

  def pitched?
    pitch.present? && pitch.pitched?
  end

  def partner_names
    cached { users.map(&:name) }
  end

  def self.sync!(quiet: true, deep: false)
    Team.for_each do |team|
      TeamCompanySyncJob.perform_later(team, deep: deep, quiet: quiet)
    end
  end

  def capital_raised(format: false)
    format ? number_to_human(super(), locale: :money) : super()
  end

  def add_user(user)
    card.add_user user
    users << user
    save!
  end

  def add_comment!(comment, notify: false)
    return unless team.present? && card.present?
    card.add_comment! comment
    team.notify!(comment, all: false) if notify
  end

  def as_json(options = {})
    super options.reverse_merge(only: [:id, :name, :description, :industry, :funded_at, :domain, :crunchbase_id, :location], methods: [:website, :complete?, :cb_url, :al_url])
  end

  def as_json_search(options = {})
    as_json options.reverse_merge(only: [:id, :name, :description, :industry, :domain], methods: [])
  end

  def as_json_api(options = {})
    options.reverse_merge!(
      methods: [:competitors],
      only: [
        :id,
        :name,
        :domain,
        :description,
      ]
    )
    key_cached(
      options.slice(:methods, :only, :except, :root, :include),
      expires_in: jitter(1, :day),
    ) do
      pitch_on = pitch.when.to_time.to_i if pitch.present?
      as_json(options).merge(
        team: team&.full_name,
        capital_raised: capital_raised(format: true),
        pitch_on: pitch_on,
        funded: funded?,
        passed: passed?,
        past_deadline: pitch&.past_deadline?,
        pitched: pitched?,
        partners: users.map { |user| { name: user.name, slack_id: user.slack_id }  },
        trello_url: card&.trello_url,
        stats: pitch&.stats,
        trello_id: card&.trello_id,
        snapshot_link: pitch&.snapshot,
      )
    end
  end

  def set_extra_attributes!
    pitch&.set_snapshot! if Rails.application.drfvote?
    set_crunchbase_attributes!
    set_angelist_attributes!
    set_competitors!
    set_capital_fields!
  end

  def invalidate_crunchbase_id!
    self.crunchbase_id = "#{Http::Crunchbase::Organization::INVALID_KEY}_#{param_name}"
    self.domain = nil
    self.description = nil
    self.capital_raised = funded? ? 20_000 : 0
    save!
  end

  def param_name
    name.gsub(' ', '').parameterize
  end

  def team
    return nil unless (cached_team = super).present?
    Team.send(cached_team.name)
  end

  def crunchbase_org(timeout = 3, raise_on_error: true)
    @crunchbase_org ||= Http::Crunchbase::Organization.new(self, timeout, raise_on_error)
  end

  def angelist_startup(timeout = 3)
    @angellist_startup ||= Http::AngelList::Startup.new(al_id, timeout: timeout)
  end

  def cb_slack_link
    cb_url.present? ? "<#{cb_url}|#{name}>" : name
  end

  def cb_url
    "https://www.crunchbase.com/organization/#{crunchbase_id}" if crunchbase_id.present?
  end

  def al_url
    "https://angel.co/startups/#{al_id}" if al_id.present?
  end

  def tweeter
    super || (Tweeter.where(username: twitter_username).first_or_create!(owner: self) if twitter_username.present?)
  end

  def competitions
    Competition.for_companies(self.id)
  end

  def competitions=(companies)
    companies.map do |company|
      Competition.from_companies!(self, company)
    end
  end

  def self.searchable_columns
    [:name]
  end

  def self.from_name(name)
    where(name: name).first_or_create!
  end

  def self.from_domain(domain)
    return nil unless domain.present?

    existing = Company.where(domain: domain).first
    return existing if existing.present?

    id = Http::Crunchbase::Organization.find_domain_id(domain, types: 'company')
    return nil unless id.present?
    Company.where(crunchbase_id: id).first_or_initialize.tap do |company|
      company.domain = domain
      company.send(:set_crunchbase_attributes!)
    end
  end

  def self.from_crunchbase_id(cb_id)
    return nil unless cb_id.present?

    existing = Company.where(crunchbase_id: cb_id).first
    return existing if existing.present?

    domain = Util.parse_domain(Http::Crunchbase::Organization.new(cb_id).homepage)
    from_domain domain
  end

  def complete?
    name.present? && description.present? && industry.present?
  end

  def humanized_industry
    return nil unless industry.present?
    industry.map { |i| Competitor::INDUSTRIES[i.to_sym] }.join(', ')
  end

  def latest_news
    @latest_news ||= news.order(created_at: :desc).limit(3)
  end

  def latest_tweets
    @latest_tweets ||= begin
      newsworthy = tweeter.newsworthy_tweets(3)
      newsworthy.present? ? newsworthy : tweeter.latest_tweets(3)
    end
  end

  def featured_competitors
    @featured_competitors ||= begin
      ids = investments
        .where(featured: true)
        .joins('INNER JOIN competitor_investor_aggs ON competitor_investor_aggs.competitor_id = investments.competitor_id')
        .order('SUM(COALESCE(competitor_investor_aggs.target_count, 0)) DESC')
        .limit(3)
        .group(:competitor_id)
        .pluck(:competitor_id)
      Competitor.where(id: ids)
    end
  end

  private

  def normalize_location
    self.location = Util.normalize_city(self.location) if self.location.present?
  end

  def start_relationships_job
    CompanyRelationshipsJob.perform_later(id)
  end

  def twitter_username
    cached { crunchbase_org(3, raise_on_error: false).twitter || angelist_startup.twitter }
  end

  def set_crunchbase_attributes!(timeout: 5)
    org = crunchbase_org(timeout)
    return unless org.found?
    self.name ||= org.name
    self.crunchbase_id = org.permalink
    self.domain = org.url
    self.description = org.description
    self.location = org.location
  rescue HTTP::Crunchbase::Errors::APIError => ex
    raise ex if Rails.application.vcwiz?
  end

  def set_angelist_attributes!(timeout: 5)
    startup = angelist_startup(timeout)
    return unless startup.found?
    self.al_id = startup.id
    self.domain ||= startup.url
    self.description ||= startup.description
    self.location ||= startup.locations.first
  rescue HTTP::AngelList::Errors::APIError => ex
    raise ex if Rails.application.vcwiz?
  end

  def set_competitors!
    self.competitors += Competitor.for_company(self)
  end

  def set_capital_fields!
    self.capital_raised = [crunchbase_org(5).total_funding.to_i || 0, funded? ? 20_000 : 0].max
    if crunchbase_org.acquisition.present?
      self.acquisition_date = Date.parse(crunchbase_org.acquisition.announced_on) rescue nil
    end
    if crunchbase_org.ipo.present?
      self.ipo_date = Date.parse(crunchbase_org.ipo.went_public_on) rescue nil
      self.ipo_valuation = crunchbase_org.ipo.opening_valuation
    end
  end

  def add_to_wit!
    return unless team.present?
    Http::Wit::Entity.new('company').add_value name
  end

  def add_graph_relationship!
    add_founder_graph_relationship!
    add_investor_graph_relationship!
  end

  def add_investor_graph_relationship!
    partners = investments.where.not(investor: nil)
    return unless partners.count > 1
    partners.each do |i1|
      partners.each do |i2|
        i1.investor.connect_to! i2.investor, :coinvest unless i1.id == i2.id
      end
    end
  end

  def add_founder_graph_relationship!
    return unless founders.count > 1
    founders.each do |f1|
      founders.each do |f2|
        f1.connect_to! f2, :cofound unless f1.id == f2.id
      end
    end
  end
end