feat(campaigns): deficiency-flag segments + LP routing (Phase 5)
Add 6 flag-based campaign segments to build_trucking_campaigns.py keyed off fmcsa_carriers.deficiency_flags: for_hire_boc3, irp_ifta, intrastate_authority, state_weight_tax (per-state LP), state_emissions (CA->ca-mcp-carb), hazmat. Each injects an order-LP link into subscriber attribs (lp_link) and only schedules when its CAMPAIGN_*_ID source template env is set (nightly run never breaks on unconfigured templates). Adds --list-segments audience report and a synthetic-data segment test (fixed a real psycopg2 % escaping bug in LIKE).
This commit is contained in:
parent
fc1a0588f7
commit
4b6c828b1c
2 changed files with 227 additions and 3 deletions
87
scripts/tests/check_campaign_segments.py
Normal file
87
scripts/tests/check_campaign_segments.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Phase-5 campaign segment validation (no production DB required).
|
||||
|
||||
Builds a synthetic in-memory fmcsa_carriers table and asserts that every
|
||||
deficiency-flag segment in build_trucking_campaigns.DEFICIENCY_SEGMENTS:
|
||||
- selects exactly the carriers carrying its flag,
|
||||
- excludes blocked-domain + already-sent + unflagged carriers,
|
||||
- routes to a valid order landing page (incl. per-state overrides).
|
||||
|
||||
Requires a reachable Postgres (DATABASE_URL) only to create a TEMP table; it
|
||||
never reads real data. Run:
|
||||
DATABASE_URL=postgresql://pw:pw_dev_2026@localhost:5434/performancewest \
|
||||
python3 scripts/tests/check_campaign_segments.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
BTC = os.path.join(ROOT, "scripts/build_trucking_campaigns.py")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not os.environ.get("DATABASE_URL"):
|
||||
print("SKIP: DATABASE_URL not set (needs any reachable Postgres for TEMP table)")
|
||||
return 0
|
||||
import psycopg2
|
||||
|
||||
spec = importlib.util.spec_from_file_location("btc", BTC)
|
||||
btc = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(btc)
|
||||
|
||||
conn = psycopg2.connect(os.environ["DATABASE_URL"])
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
CREATE TEMP TABLE fmcsa_carriers (
|
||||
dot_number text, email_address text, legal_name text, phy_state text,
|
||||
mcs150_parsed timestamptz, oos_active boolean,
|
||||
email_verified boolean, email_verify_result text,
|
||||
listmonk_sent_at timestamptz, deficiency_flags text[]);
|
||||
""")
|
||||
rows = [
|
||||
("1", "a@good.com", "For Hire Co", "NY", None, False, True, None, None, ["for_hire_carrier"]),
|
||||
("2", "b@good.com", "IRP Co", "TX", None, False, True, None, None, ["interstate_needs_irp_ifta"]),
|
||||
("3", "c@good.com", "Intrastate Co", "CA", None, False, True, None, None, ["intrastate_authority_CA"]),
|
||||
("4", "d@good.com", "Weight Tax Co", "OR", None, False, True, None, None, ["state_weight_tax_OR"]),
|
||||
("5", "e@good.com", "Emissions Co", "CO", None, False, True, None, None, ["state_emissions_CO"]),
|
||||
("6", "f@good.com", "Hazmat Co", "OH", None, False, True, None, None, ["hazmat_carrier"]),
|
||||
("7", "g@good.com", "Clean Co", "NY", None, False, True, None, None, []), # no flags
|
||||
("8", "h@aol.com", "Blocked Co", "NY", None, False, True, None, None, ["for_hire_carrier"]), # blocked domain
|
||||
("9", "i@good.com", "Sent Co", "NY", None, False, True, None, "2026-01-01", ["for_hire_carrier"]), # already sent
|
||||
]
|
||||
for r in rows:
|
||||
cur.execute("INSERT INTO fmcsa_carriers VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", r)
|
||||
conn.commit()
|
||||
|
||||
all_states = tuple(s for cfg in btc.TIMEZONE_CONFIG.values() for s in cfg["states"])
|
||||
expected = {
|
||||
"for_hire_boc3": 1, "irp_ifta": 1, "intrastate_authority": 1,
|
||||
"state_weight_tax": 1, "state_emissions": 1, "hazmat": 1,
|
||||
}
|
||||
ok = True
|
||||
for ctype in btc.DEFICIENCY_SEGMENTS:
|
||||
got = len(btc.fetch_carriers(conn, all_states, ctype, 1000))
|
||||
status = "OK" if got == expected[ctype] else "FAIL"
|
||||
if got != expected[ctype]:
|
||||
ok = False
|
||||
print(f" {status}: {ctype:<22} matched {got} (expected {expected[ctype]})")
|
||||
|
||||
lp_ok = (
|
||||
btc.build_lp_link("state_weight_tax", "OR").endswith("/order/or-weight-mile-tax")
|
||||
and btc.build_lp_link("state_emissions", "CA").endswith("/order/ca-mcp-carb")
|
||||
and btc.build_lp_link("hazmat", None).endswith("/order/hazmat-phmsa")
|
||||
and btc.build_lp_link("irp_ifta", None).endswith("/order/state-trucking-bundle")
|
||||
)
|
||||
print(" LP routing:", "OK" if lp_ok else "FAIL")
|
||||
conn.close()
|
||||
|
||||
passed = ok and lp_ok
|
||||
print("CAMPAIGN SEGMENTS:", "PASS" if passed else "FAIL")
|
||||
return 0 if passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue