#!/usr/bin/env python3 """ Tier 1 Lead Scorer - Progressive Filtering - Runs parallel to JAE v5 - Scores leads as they're processed - Maintains dynamic top 50 list - Updates in real-time """ import json, re, time from datetime import datetime from pathlib import Path SCRIPT_DIR = Path(__file__).parent STATE_FILE = SCRIPT_DIR / "state" / "tier1-state.json" SCORED_FILE = SCRIPT_DIR / "state" / "tier1-scored-leads.json" CRM_URL = "https://salesforce.hoaledgeriq.com/rest" CRM_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5M2FmNGFmNS0zZWQ0LTQ1ZDMtOWE5Zi01MDMzZjc3YTY3MjMiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiOTNhZjRhZjUtM2VkNC00NWQzLTlhOWYtNTAzM2Y3N2E2NzIzIiwiaWF0IjoxNzczMzI4NDQzLCJleHAiOjE4MDQ3ODE2NDIsImp0aSI6IjIwZjEyYzkwLTRkMDctNGJmNi1iMzk3LTZjNmU3MzlmMThjOCJ9.zeM5NvwCSGEcz99m2LYtgb0sVD6WUXcCF7SwonFg930" def log(msg): ts = datetime.now().strftime('%H:%M:%S') print(f"[{ts}] {msg}") def load_state(): if STATE_FILE.exists(): return json.loads(STATE_FILE.read_text()) return {"last_processed": 0, "tier1_leads": [], "scoring_rules": { "unit_range_ideal": [150, 400], "unit_range_acceptable": [100, 500], "require_budget_pdf": False, "min_score": 6 }} def save_state(s): STATE_FILE.write_text(json.dumps(s, indent=2)) def extract_units_from_note(note): """Extract unit count from note body or title""" body = note.get('bodyV2', {}).get('markdown', '') if isinstance(note.get('bodyV2'), dict) else '' title = note.get('title', '') text = f"{title} {body}".lower() # Look for unit patterns patterns = [ r'(\d{1,4})\s*(?:homes|units|lots|properties|residences)', r'(\d{1,4})\s*-?\s*(?:home|unit|lot|property|residence)\s*(?:community|association|complex)', r'community\s*of\s*(\d{1,4})', r'units:\s*(\d{1,4})', ] for pattern in patterns: match = re.search(pattern, text, re.IGNORECASE) if match: try: units = int(match.group(1)) if 10 <= units <= 5000: # Reasonable range return units except: pass return None def has_budget_pdf(note): """Check if note has budget PDF""" body = note.get('bodyV2', {}).get('markdown', '') if isinstance(note.get('bodyV2'), dict) else '' title = note.get('title', '') text = f"{title} {body}".lower() # Check for budget PDF mentions if 'budget pdf' in text or 'budget.pdf' in text or 'found budget pdf' in text: return True if 'budget found' in text and 'pdf' in text: return True return False def has_website(note): """Check if note has website""" body = note.get('bodyV2', {}).get('markdown', '') if isinstance(note.get('bodyV2'), dict) else '' title = note.get('title', '') text = f"{title} {body}" return 'https://' in text or 'http://' in text def get_temp(note): """Get temperature from note""" temp = note.get('temp', 'COLD') if temp and temp.upper() in ['HOT', 'WARM', 'COLD']: return temp.upper() title = note.get('title', '').upper() if title.startswith('HOT:'): return 'HOT' if title.startswith('WARM:'): return 'WARM' return 'COLD' def score_lead(note): """ Score a lead 1-10 based on Tier 1 criteria Returns: (score, breakdown) """ temp = get_temp(note) units = extract_units_from_note(note) budget_pdf = has_budget_pdf(note) has_site = has_website(note) score = 0 breakdown = [] # 1. Temperature (max 3 points) if temp == 'HOT': score += 3 breakdown.append("Temp: HOT (+3)") elif temp == 'WARM': score += 2 breakdown.append("Temp: WARM (+2)") else: breakdown.append("Temp: COLD (+0)") # 2. Unit Count (max 4 points) if units: if 150 <= units <= 400: score += 4 breakdown.append(f"Units: {units} (ideal range +4)") elif 100 <= units < 150 or 400 < units <= 500: score += 3 breakdown.append(f"Units: {units} (good range +3)") elif 50 <= units < 100 or 500 < units <= 1000: score += 2 breakdown.append(f"Units: {units} (acceptable +2)") else: score += 1 breakdown.append(f"Units: {units} (outside ideal +1)") else: breakdown.append("Units: Unknown (0)") # 3. Budget PDF (max 2 points) if budget_pdf: score += 2 breakdown.append("Budget PDF: Found (+2)") elif has_site: score += 1 breakdown.append("Budget: Mentioned (+1)") else: breakdown.append("Budget: Not found (0)") # 4. Website Quality (max 1 point) if has_site: score += 1 breakdown.append("Website: Yes (+1)") return score, breakdown def fetch_recent_notes(limit=200): """Fetch recent notes from CRM""" import urllib.request, ssl, json ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE try: req = urllib.request.Request( f"{CRM_URL}/notes?limit={limit}&order[createdAt]=desc", headers={"Authorization": f"Bearer {CRM_TOKEN}", "Accept": "application/json"} ) opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ssl_context)) with opener.open(req, timeout=30) as r: data = json.loads(r.read().decode()) return data.get('data', {}).get('notes', []) except Exception as e: log(f"Fetch error: {e}") return [] def update_crm_note(note_id, score, tier1_label): """Update CRM note with tier1_score field""" import urllib.request, ssl, json ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE try: # Patch the note to add tier1_score patch_data = json.dumps({ "tier1Score": score, "tier1Label": tier1_label }).encode() req = urllib.request.Request( f"{CRM_URL}/notes/{note_id}", data=patch_data, headers={ "Authorization": f"Bearer {CRM_TOKEN}", "Content-Type": "application/json" }, method='PATCH' ) opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ssl_context)) with opener.open(req, timeout=20) as r: return True except Exception as e: log(f" āš ļø CRM update failed: {e}") return False def main(): log("=" * 60) log("Tier 1 Scorer - Starting") log("=" * 60) state = load_state() scored_leads = state.get('tier1_leads', []) last_processed = state.get('last_processed', 0) log(f"Loading {len(scored_leads)} previously scored leads") # Fetch recent notes notes = fetch_recent_notes(500) log(f"Fetched {len(notes)} recent notes") new_additions = 0 updated_additions = 0 crm_updates = 0 for i, note in enumerate(notes): note_id = note.get('id') title = note.get('title', '')[:50] # Score the lead score, breakdown = score_lead(note) # Determine Tier 1 label if score >= 8: tier1_label = "Tier 1 - Priority" elif score >= 6: tier1_label = "Tier 1" else: tier1_label = "" # Check if already in scored list existing = next((x for x in scored_leads if x['id'] == note_id), None) if score >= 6: # Minimum threshold for Tier 1 consideration lead_data = { 'id': note_id, 'title': title, 'score': score, 'breakdown': breakdown, 'temp': get_temp(note), 'units': extract_units_from_note(note), 'budget_pdf': has_budget_pdf(note), 'updated': datetime.now().isoformat() } if existing: # Update existing if existing['score'] != score: existing.update(lead_data) updated_additions += 1 # Update CRM if update_crm_note(note_id, score, tier1_label): crm_updates += 1 log(f" āœ“ Updated CRM: {title[:40]} (Score: {score}/10)") else: # Add new scored_leads.append(lead_data) new_additions += 1 # Update CRM if update_crm_note(note_id, score, tier1_label): crm_updates += 1 log(f" āœ“ Updated CRM: {title[:40]} (Score: {score}/10)") # Sort by score (descending) scored_leads.sort(key=lambda x: x['score'], reverse=True) # Keep only top 100 for now scored_leads = scored_leads[:100] # Save state state['tier1_leads'] = scored_leads state['last_processed'] = len(notes) save_state(state) log(f"\n=== Tier 1 Results ===") log(f"Total scored: {len(scored_leads)}") log(f"New additions: {new_additions}") log(f"Updates: {updated_additions}") log(f"CRM updates: {crm_updates}") if scored_leads: log(f"\nTop 10 Tier 1 Leads:") for i, lead in enumerate(scored_leads[:10], 1): units_str = f"{lead['units']} units" if lead['units'] else "units: ?" log(f" {i}. [{lead['score']}/10] {lead['title'][:40]} ({lead['temp']}, {units_str})") log("\n" + "=" * 60) log(f"Tier 1 scoring complete. Top lead score: {scored_leads[0]['score'] if scored_leads else 0}/10") log("=" * 60) log(f"\nšŸ“Š CRM Integration:") log(f" • Field: tier1_score (numeric)") log(f" • Label: tier1_label (text)") log(f" • Filter view: tier1_score >= 6") log("=" * 60) if __name__ == "__main__": main()