#!/usr/bin/env python3 """Build the OTC issuer reincorporation/compliance outreach campaign. Audience: US-domestic OTC SEC issuers (from harvest_otc_issuers.py + scrape_otc_emails.py + verify). Hook: redomesticate-to-Texas for DE/NV issuers; cross-sell registered agent / foreign qualification / annual report / franchise tax. High per-deal value, small universe (~700-900), so NOT warmup-capped the same way -- but still MX-aware and same-day-coupon enabled. Reuses the trucking sender plumbing. Input CSV (verified): cik,name,ticker,state_inc,phone,city,state,zip,domain,email plus verify columns. Only rows with a verified email are emailed. Usage: python3 scripts/build_otc_campaign.py VERIFIED.csv --start-campaign python3 scripts/build_otc_campaign.py VERIFIED.csv --preview python3 scripts/build_otc_campaign.py VERIFIED.csv --dry-run """ from __future__ import annotations import argparse import csv import os import sys from datetime import datetime, timezone, timedelta, date ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if ROOT not in sys.path: sys.path.insert(0, ROOT) import psycopg2 # noqa: E402 import scripts.build_trucking_campaigns as tc # noqa: E402 LOG = tc.LOG SOURCE_ENV = "CAMPAIGN_OTC_REINCORP_ID" LP_LINK = f"{tc.SITE_DOMAIN}/contact?service=reincorporation" STATE_NAMES = { "DE": "Delaware", "NV": "Nevada", "CA": "California", "NY": "New York", "FL": "Florida", "TX": "Texas", "CO": "Colorado", "WY": "Wyoming", "MD": "Maryland", "IA": "Iowa", "MN": "Minnesota", "NJ": "New Jersey", } def verified_ok(r: dict) -> bool: e = (r.get("email") or "").strip() if not e or "@" not in e: return False vr = (r.get("verify_ok") or r.get("verify_reason") or "").strip().lower() # Accept if verifier passed, or if the column is absent (treat present email # as usable -- but prefer running verify first). if "verify_ok" in r: return str(r.get("verify_ok")).strip().upper() in ("Y", "YES", "TRUE", "1") return True def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("infile") ap.add_argument("--start-campaign", action="store_true") ap.add_argument("--preview", action="store_true") ap.add_argument("--dry-run", action="store_true") ap.add_argument("--limit", type=int, default=int(os.getenv("OTC_DAILY_CAP", "400"))) ap.add_argument("--de-nv-only", action="store_true", help="only Delaware/Nevada issuers (best reincorp fit)") args = ap.parse_args() import logging logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"), format="%(asctime)s %(levelname)s %(message)s") rows = [r for r in csv.DictReader(open(args.infile, newline="", encoding="utf-8")) if verified_ok(r)] if args.de_nv_only: rows = [r for r in rows if (r.get("state_inc") or "").strip() in ("DE", "NV")] LOG.info("[otc] %d emailable issuers (de_nv_only=%s)", len(rows), args.de_nv_only) src = os.getenv(SOURCE_ENV) if not src: LOG.error("[otc] %s not set -- create the OTC source campaign first", SOURCE_ENV) return 2 base = tc.get_base_campaign(int(src)) conn = psycopg2.connect(tc.DB_URL) coupon = None if args.start_campaign and not args.preview and not args.dry_run: try: coupon = tc.get_or_create_daily_coupon(conn, date.today()) except Exception as exc: # noqa: BLE001 LOG.warning("[otc] coupon mint failed: %s", exc) def attribs(r): si = (r.get("state_inc") or "").strip() return { "company": r.get("name", ""), "ticker": r.get("ticker", ""), "state_inc": si, "state_inc_name": STATE_NAMES.get(si, si or "your state"), "lp_link": LP_LINK, **tc.coupon_attribs(coupon), } if args.preview: subs = [{"email": tc.TEST_EMAIL, "name": "Sample Issuer", "attribs": attribs({"name": "Sample Issuer Inc.", "ticker": "SMPL", "state_inc": "DE"})}] else: subs = [] seen = set() for r in rows[:args.limit]: e = r["email"].strip().lower() if e in seen: continue seen.add(e) subs.append({"email": e, "name": r.get("name", e), "attribs": attribs(r)}) today = date.today().isoformat() list_name = f"OTC Reincorporation - {today}" campaign_name = f"OTC Reincorporation - {date.today().strftime('%b %d %Y')}" if args.dry_run: LOG.info("[otc] DRY RUN -- would email %d issuers (coupon=%s)", len(subs), coupon) return 0 list_id = tc.create_list(list_name) added = tc.import_subscribers(list_id, subs) LOG.info("[otc] list %d: %d/%d added", list_id, added, len(subs)) if added == 0: LOG.error("[otc] 0 added; aborting") return 1 send_at = datetime.now(timezone.utc) + timedelta(minutes=5) cid = tc.create_and_schedule_campaign( base, list_id, campaign_name, send_at, schedule=args.start_campaign and not args.preview) LOG.info("[otc] campaign %d created (%s)", cid, "scheduled" if args.start_campaign and not args.preview else "draft") conn.close() return 0 if __name__ == "__main__": raise SystemExit(main())