#!/usr/bin/env python3 """ Tier 1 Scorer - Full Database Scan - Processes ALL leads JAE v5 has researched - Updates CRM fields directly - Creates filterable views """ import json, re, time, urllib.request, ssl from datetime import datetime from pathlib import Path SCRIPT_DIR = Path(__file__).parent STATE_FILE = SCRIPT_DIR / "state" / "tier1-state.json" JAE_STATE = SCRIPT_DIR / "state" / "jae-v5-state.json" CRM_URL = "https://salesforce.hoaledgeriq.com/rest" CRM_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5M2FmNGFmNS0zZWQ0LTQ1ZDMtOWE5Zi01MDMzZjc3YTY3MjMiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiOTNhZjRhZjUtM2VkNC00NWQzLTlhOWYtNTAzM2Y3N2E2NzIzIiwiaWF0IjoxNzczMzI4NDQzLCJleHAiOjE4MDQ3ODE2NDIsImp0aSI6IjIwZjEyYzkwLTRkMDctNGJmNi1iMzk3LTZjNmU3MzlmMThjOCJ9.zeM5NvwCSGEcz99m2LYtgb0sVD6WUXcCF7SwonFg930" ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE def log(msg): ts = datetime.now().strftime('%H:%M:%S') print(f"[{ts}] {msg}") def fetch_all_notes_paginated(): """Fetch all notes with pagination""" all_notes = [] has_more = True end_cursor = None log("Fetching all leads from CRM (with pagination)...") while has_more: try: url = f"{CRM_URL}/notes?limit=200&order[createdAt]=desc" if end_cursor: url += f"&after={end_cursor}" req = urllib.request.Request( url, 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()) notes = data.get('data', {}).get('notes', []) all_notes.extend(notes) # Check pagination page_info = data.get('pageInfo', {}) has_more = page_info.get('hasNextPage', False) end_cursor = page_info.get('endCursor') log(f" Fetched {len(notes)} leads (total: {len(all_notes)})") if not has_more: break except Exception as e: log(f"Fetch error: {e}") break log(f"Total leads fetched: {len(all_notes)}") return all_notes 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'units:\s*(\d{1,4})', r'(\d{1,4})\s*(?:homes|units|lots|properties|residences)', r'community\s*of\s*(\d{1,4})', r'(\d{1,4})\s*home\s*owners', ] for pattern in patterns: match = re.search(pattern, text, re.IGNORECASE) if match: try: units = int(match.group(1)) if 10 <= units <= 5000: 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 mentions (JAE v5 format) if 'budget pdf' in text or 'budget.pdf' in text or 'found budget pdf' in text: return True # Also check title patterns from JAE research if 'budget' in text and 'found' 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""" temp = get_temp(note) units = extract_units_from_note(note) budget_pdf = has_budget_pdf(note) has_site = has_website(note) score = 0 # 1. Temperature (max 3 points) if temp == 'HOT': score += 3 elif temp == 'WARM': score += 2 # 2. Unit Count (max 4 points) if units: if 150 <= units <= 400: score += 4 elif 100 <= units < 150 or 400 < units <= 500: score += 3 elif 50 <= units < 100 or 500 < units <= 1000: score += 2 else: score += 1 # 3. Budget PDF (max 2 points) if budget_pdf: score += 2 elif has_site: score += 1 # 4. Website (max 1 point) if has_site: score += 1 return score def update_crm_note(note_id, score, tier1_label): """Update CRM note with tier1_score field""" try: 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: return False def main(): log("=" * 60) log("Tier 1 Scorer - Full Database Scan") log("=" * 60) # Fetch all notes notes = fetch_all_notes_paginated() total_scored = 0 crm_updates = 0 tier1_count = 0 for i, note in enumerate(notes): note_id = note.get('id') title = note.get('title', '')[:50] # Score the lead score = 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 = "" # Update CRM if score is 6+ if score >= 6: total_scored += 1 if update_crm_note(note_id, score, tier1_label): crm_updates += 1 tier1_count += 1 if tier1_count <= 10: # Show first 10 log(f" āœ“ {title[:40]} (Score: {score}/10)") # Progress indicator if (i + 1) % 100 == 0: log(f"Processed {i+1}/{len(notes)} leads...") log("\n" + "=" * 60) log(f"Tier 1 Scoring Complete!") log(f" Total leads processed: {len(notes)}") log(f" Tier 1 leads (6+): {tier1_count}") log(f" CRM updates: {crm_updates}") log(f"\nšŸ“Š Filter in CRM:") log(f" • View: tier1_score >= 6") log(f" • Sort by: tier1_score DESC") log("=" * 60) if __name__ == "__main__": main()