← back to Rentv Adintel

lib/types.js

147 lines

'use strict';
/**
 * Shared contract — canonical enumerated value lists (spec §2, §8, §10, §14).
 * Every module (seed, routes, adapters, export) imports from here so the
 * classification vocabulary has ONE source of truth. Mirrors db/schema.sql
 * CHECK domains without hard-coding them into the DDL (keeps migrations simple).
 */

// §2 — advertising relationship status
const RELATIONSHIP_STATUS = Object.freeze([
  'VERIFIED_ADVERTISER',
  'VERIFIED_CONFERENCE_SPONSOR',
  'VERIFIED_EXHIBITOR',
  'VERIFIED_MEDIA_PARTNER',
  'VERIFIED_CONTENT_PARTNER',
  'SPEAKER_OR_PANELIST_ONLY',
  'PAST_ADVERTISER',
  'LIKELY_PROSPECT',
  'RESEARCH_NEEDED',
  'DISQUALIFIED',
]);

// Which statuses count as "verified" for the Verified-Only switch (§25).
const VERIFIED_STATUSES = Object.freeze([
  'VERIFIED_ADVERTISER',
  'VERIFIED_CONFERENCE_SPONSOR',
  'VERIFIED_EXHIBITOR',
  'VERIFIED_MEDIA_PARTNER',
  'VERIFIED_CONTENT_PARTNER',
]);

// Plain-English labels for Simple View (§25 — no jargon, no color-only badges).
const STATUS_LABELS = Object.freeze({
  VERIFIED_ADVERTISER: 'Verified advertiser',
  VERIFIED_CONFERENCE_SPONSOR: 'Verified conference sponsor',
  VERIFIED_EXHIBITOR: 'Verified exhibitor',
  VERIFIED_MEDIA_PARTNER: 'Verified media partner',
  VERIFIED_CONTENT_PARTNER: 'Content partner (not a paid sponsor)',
  SPEAKER_OR_PANELIST_ONLY: 'Speaker / panelist only',
  PAST_ADVERTISER: 'Past advertiser',
  LIKELY_PROSPECT: 'Likely prospect',
  RESEARCH_NEEDED: 'Research needed',
  DISQUALIFIED: 'Disqualified',
});

// §10 — source access methods
const SOURCE_ACCESS_METHODS = Object.freeze([
  'official_api',
  'official_bulk_download',
  'rss_or_sitemap',
  'first_party_public_web',
  'authorized_mailbox',
  'manual_upload',
  'manual_review_only',
]);

// §9 — search providers (manual default, never scrape result HTML)
const SEARCH_PROVIDERS = Object.freeze(['google_cse', 'brave', 'bing', 'serper', 'manual']);

// §10 — evidence types
const EVIDENCE_TYPES = Object.freeze([
  'WEB_PAGE', 'EMAIL', 'PDF', 'IMAGE', 'SCREENSHOT', 'CSV', 'API', 'MANUAL_NOTE',
]);

// §11 — creative rights
const RIGHTS_STATUS = Object.freeze([
  'INTERNAL_EVIDENCE_ONLY', 'EXPORT_ALLOWED', 'LINK_ONLY', 'UNKNOWN',
]);

// §14 — contact role priority (index 0 = highest priority)
const CONTACT_ROLE_PRIORITY = Object.freeze([
  'CHIEF_MARKETING_OFFICER',
  'VP_MARKETING',
  'MARKETING_DIRECTOR',
  'COMMUNICATIONS_PR_DIRECTOR',
  'EVENTS_PARTNERSHIPS_SPONSORSHIPS_DIRECTOR',
  'BUSINESS_DEVELOPMENT_DIRECTOR',
  'REGIONAL_PRESIDENT_MARKET_LEADER',
  'MANAGING_DIRECTOR_PRINCIPAL',
  'PUBLIC_MEDIA_CONTACT',
  'GENERAL_COMPANY_CONTACT',
]);

// §8 — advertiser category taxonomy
const ADVERTISER_CATEGORIES = Object.freeze([
  'Brokerage and investment sales',
  'Leasing and tenant representation',
  'Developer, owner, investor, REIT, and family office',
  'Commercial bank and credit union',
  'Debt fund, mortgage bank, private lender, and capital advisor',
  'Title, escrow, settlement, and 1031 exchange',
  'Law firm',
  'Accounting, tax, and advisory',
  'Architecture, interiors, planning, and design',
  'General contractor and construction manager',
  'Engineering, environmental, surveying, and testing',
  'Property and facility management',
  'Insurance and risk management',
  'Appraisal, valuation, and due diligence',
  'Proptech, data, software, AI, and marketplace',
  'Security, telecom, access control, energy, solar, and building systems',
  'Furniture, finishes, materials, wallcovering, and workplace products',
  'Economic development organization, municipality, port, and public agency',
  'Coworking, flexible office, and business services',
  'Auction, disposition, and receivership services',
  'Recruiting, staffing, education, and certification',
  'Association, conference, publication, and media company',
  'Hospitality, venue, catering, transportation, and event vendor',
  'Other CRE service',
]);

// LinkedIn hosts that the generic fetcher MUST block (§6.4, §32).
const LINKEDIN_BLOCKED_HOSTS = Object.freeze([
  'linkedin.com', 'www.linkedin.com', 'm.linkedin.com', 'lnkd.in',
]);

/** normalize a company/person name for deterministic matching (§12). */
function normalizeName(s) {
  return String(s || '')
    .toLowerCase()
    .normalize('NFKD')
    .replace(/[̀-ͯ]/g, '')
    .replace(/\b(the|inc|inc\.|llc|l\.l\.c\.|corp|corporation|company|co|co\.|group|lp|l\.p\.|ltd)\b/g, '')
    .replace(/[^a-z0-9]+/g, ' ')
    .trim()
    .replace(/\s+/g, ' ');
}

function isVerified(status) {
  return VERIFIED_STATUSES.includes(status);
}

module.exports = {
  RELATIONSHIP_STATUS,
  VERIFIED_STATUSES,
  STATUS_LABELS,
  SOURCE_ACCESS_METHODS,
  SEARCH_PROVIDERS,
  EVIDENCE_TYPES,
  RIGHTS_STATUS,
  CONTACT_ROLE_PRIORITY,
  ADVERTISER_CATEGORIES,
  LINKEDIN_BLOCKED_HOSTS,
  normalizeName,
  isVerified,
};