<aside> ⚙️

Canonical reference for the FSP Marketing Dashboard Funnel. Every per-tab metrics page cites this page for the shared data sources, base filters, product scopes, timezone handling, and caching rules. All rules below are verified against source; each is cited as path:line.

</aside>

Data Sources & Connections

The app spans three PostgreSQL connections, wired in config/database.yml. Every ActiveRecord model inherits from the base class for its connection.

<aside> 💡

In test, both fsp_sql and fsp_api_hub are pointed at the local fsp_marketing_dash_test database (see the Rails.env.test? ternaries in config/database.yml). Dev and prod both connect to the same AWS RDS instance.

</aside>

Primary keys & join keys

Canonical deal→contact join:

SELECT ...
FROM hubspot_deals
INNER JOIN hubspot_contacts
  ON hubspot_deals.contact_id = hubspot_contacts.hs_object_id

New Contacts Base Filter (F2P)

The "New Contacts" universe. Composed scope HubspotContact.new_contacts_base (app/models/hubspot_contact.rb:23-32), chaining the eight predicate scopes at app/models/hubspot_contact.rb:11-20.

lead_type = 'Residential'
AND lead_source IS NOT NULL
AND (NOT ost OR ost IS NULL)
AND aged_lead IS NOT TRUE
AND (NOT return_accepted OR return_accepted IS NULL)
AND phone IS NOT NULL
AND (NOT is_battery_auto_quote OR is_battery_auto_quote IS NULL)
AND hs_analytics_source_data_1 NOT IN ('CONVERSATIONS', 'EXTENSION')

<aside> 💡

Contacted stage uses a LOOSER base. contacted_leads_base (app/models/hubspot_contact.rb:39-46) drops the phone IS NOT NULL and hs_analytics_source_data_1 NOT IN (...) predicates — it keeps only residential + with_lead_source + non_ost + non_aged + non_return + non_auto_quote. This mirrors f2p contacted_leads.py. Do not assume New Leads and Contacted share the same denominator.

</aside>

The CLAUDE.md "New Contacts base filter" list matches the code exactly. Its note that contacted is looser is captured by the separate contacted_leads_base scope.