The daily 40%-off coupon was being merged into every trucking/UCR/IFTA/OTC
send, but those discount sends were not actually being delivered (the
DKIM-broken window). Now that deliverability is fixed, re-test whether
normal-price offers convert before giving margin away.
New CAMPAIGN_ENABLE_COUPON env flag (default OFF) gates daily-coupon
minting in build_trucking_campaigns + the UCR/IFTA/OTC builders (which
import it as tc.COUPON_ENABLED). With it off, no code is minted and an
empty coupon_code is merged -> the campaign templates' existing
{{ if .Subscriber.Attribs.coupon_code }} guard falls through to the
normal-price {{ else }} branch and landing-page links carry no ?code=.
No template or DB changes; fully reversible (set CAMPAIGN_ENABLE_COUPON=1).
Verified: COUPON_ENABLED defaults False, coupon_attribs(None) -> empty,
lp_link drops ?code= when no coupon, all 4 builders compile.
141 lines
5.4 KiB
Python
141 lines
5.4 KiB
Python
#!/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 tc.COUPON_ENABLED and 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)
|
|
elif not tc.COUPON_ENABLED:
|
|
LOG.info("[otc] coupon disabled (CAMPAIGN_ENABLE_COUPON unset) — normal price")
|
|
|
|
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())
|