Class: PartyResearch::Adapters::LogoDev

Inherits:
Base
  • Object
show all
Defined in:
app/services/party_research/adapters/logo_dev.rb

Overview

Logo.dev Brand API adapter — a cheap, deterministic lookup keyed on a
company's web domain. We take two things from the payload: the brand's
public social profiles and its one-line business description.

Everything else Logo.dev returns is deliberately ignored. The logo/
brandmark imagery is already covered by PartyProfileImageLookupService
via the image CDN (which costs no credits), colors and
logo_blurhash have no consumer here, and name is the marketing brand
string ("sweetgreen") rather than the legal entity name the CRM tracks.

Complementary to Apollo/PDL rather than a replacement: those resolve
people and firmographics off a name, this resolves what a domain
publicly presents. Because it needs nothing but a domain and costs one
request, it's the only adapter cheap enough to run at customer-creation
time — see the only: %w[logo_dev] enqueue in
Customer::NewCustomer#build_and_save_customer. The full adapter set
still runs later when the lead reaches lead_qualify.

Auth: a secret key (sk_...) as a bearer token, server-side only.
This is NOT the publishable key embedded in img.logo.dev URLs:

logo_dev:
publishable_key: pk_xxx # image CDN (existing)
secret_key: sk_xxx # Brand API (this adapter)

Cost: 5 credits per call against a monthly grant (Pro = 15,000 credits
≈ 3,000 calls). Responses are cached per domain because brand profiles
change on the order of years, and 404s are cached too so an unknown
domain is only paid for once.

Constant Summary collapse

ENDPOINT =
'https://api.logo.dev/brand'
TIMEOUT_SECONDS =

Logo.dev indexes on demand and documents responses taking up to 30s;
their floor is 15. Nothing is waiting on this — it runs on the low
Sidekiq queue — so take the generous end.

30
CACHE_TTL =
30.days
SOCIAL_PLATFORMS =

Logo.dev detects 13 platforms; we map only the five that have a
real ContactPoint category. The others (youtube, github, reddit,
telegram, tiktok…) are dropped on purpose: FindingApplier falls
back to the generic website category for platforms it doesn't
recognize, and a YouTube URL sitting in the website contact point
would then be mistaken for the company domain by every later
lookup — including this adapter's own company_domain.

Each maps to the ContactPoint format validation it must satisfy.
Reusing the model's own constants means a proposal can't drift from
what Accept will actually let through — and a finding that fails
validation is worse than no finding, because the reviewer only
discovers it when the accept button blows up.

{
  'facebook' => ContactPoint::FACEBOOK_REGEX,
  'twitter' => ContactPoint::TWITTER_REGEX,
  'linkedin' => ContactPoint::LINKEDIN_REGEX,
  'instagram' => ContactPoint::INSTAGRAM_REGEX,
  'pinterest' => ContactPoint::PINTEREST_REGEX
}.freeze

Constants inherited from Base

Base::ALL, Base::PERSONAL_EMAIL_DOMAINS

Instance Attribute Summary

Attributes inherited from Base

#party, #raw_findings

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

adapter_name, arbiter?, #finding, #initialize, lookup, registered

Constructor Details

This class inherits a constructor from PartyResearch::Adapters::Base

Class Method Details

.company_domain(party) ⇒ Object

Company domain, in priority order:

  1. The party's website contact point
  2. A non-webmail email domain on the party
  3. A non-webmail email domain on one of its contacts

Contacts' websites are deliberately not consulted — those are
routinely third-party portals (CommerceHub and friends), not the
customer's own domain.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'app/services/party_research/adapters/logo_dev.rb', line 88

def company_domain(party)
  return nil unless party.respond_to?(:contact_points)

  website = party.contact_points.websites.first&.detail
  host = host_from(website)
  return host if host.present?

  emails = party.contact_points.emails.pluck(:detail)
  # One query across every contact rather than one per contact —
  # this runs inside `relevant_for?`, which the picker UI calls for
  # each adapter on page load.
  emails += ContactPoint.emails.where(party_id: party.contact_ids).order(:id).pluck(:detail) if party.is_a?(Customer)

  emails.filter_map { |email| domain_from_email(email) }.first
end

.configured?Boolean

Returns:

  • (Boolean)


66
67
68
# File 'app/services/party_research/adapters/logo_dev.rb', line 66

def configured?
  Heatwave::Configuration.fetch(:logo_dev, :secret_key).present?
end

.relevant_for?(party) ⇒ Boolean

The Brand API is keyed on a company domain, so this is useful
only for Customers we can resolve one for. Homeowners and
webmail-only leads have nothing to look up.

Returns:

  • (Boolean)


73
74
75
76
77
78
# File 'app/services/party_research/adapters/logo_dev.rb', line 73

def relevant_for?(party)
  return false unless party.is_a?(Customer)
  return false if party.try(:homeowner?)

  company_domain(party).present?
end

Instance Method Details

#findings_forObject



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'app/services/party_research/adapters/logo_dev.rb', line 126

def findings_for
  unless self.class.configured?
    raise ConfigurationError,
          'Logo.dev secret_key is not configured (Heatwave::Configuration[:logo_dev, :secret_key])'
  end

  domain = self.class.company_domain(party)
  return [] if domain.blank?

  brand = fetch(domain)
  return [] if brand.blank?

  rationale = "Logo.dev Brand profile for #{domain}"
  social_findings(brand, rationale) + description_findings(brand, rationale)
end