otc: reincorporation email template + campaign builder
otc_reincorporation.html: redomesticate-to-Texas hook (Business Court + TXSE + DE franchise-tax cost) personalized by state_inc_name/company/ticker, cross-sell RA/foreign-qual/annual-report/franchise-tax, same-day coupon, lead-capture CTA to /contact?service=reincorporation (high-touch corporate service, not self-serve), careful 'not a law firm / not legal advice' disclaimers + CAN-SPAM address. build_otc_campaign.py: emails only verified-email issuers from the harvest+scrape +verify pipeline, --de-nv-only for the best reincorp fit, reuses trucking sender plumbing + coupon. Per-deal value is high so capped modestly (400/run default).
This commit is contained in:
parent
4d3af2aeae
commit
134a2611f6
2 changed files with 196 additions and 0 deletions
139
scripts/build_otc_campaign.py
Normal file
139
scripts/build_otc_campaign.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue