trucking: same-day expiring coupon to drive immediate conversion
The sales we got came at $79 + a 24hr coupon; cutting MCS-150 to $39 flat removed urgency and conversions did NOT improve (a permanent low price sets a new anchor and lets people defer). Restore the higher anchor and let an expiring discount create the now-or-lose-it decision. - Restore MCS-150 anchor $39 -> $79 (catalog single source + regenerated). - build_trucking_campaigns.py: mint ONE random 5-letter coupon per send-day (40% off, valid through 23:59:59 ET that day) into the existing discount_codes table; inject coupon_code/pct/expires + a ?code= LP link into every email. Idempotent per day; service-fee-only scope (gov/pass-through fees never cut). - Listmonk MCS-150 (186) + Inactive USDOT (188) templates: lead with the struck-through anchor + sale price + code + 'expires tonight', and point the primary CTA at the order page (with code) instead of the 'free check' tool. - OrderPriceBanner: validates ?code= via /api/v1/discount and shows was/now + expiry; Wizard forwards the code to order creation. - Verified: code gen, expiry math, scope enforcement, discount API (40% off $79 = $47.40), site+api builds clean.
This commit is contained in:
parent
dd4ed3ea38
commit
5e9aec40d1
30 changed files with 216 additions and 31 deletions
|
|
@ -135,6 +135,111 @@ def build_lp_link(campaign_type: str, phy_state: str | None) -> str:
|
|||
slug = "ca-mcp-carb"
|
||||
return f"{SITE_DOMAIN}/order/{slug}"
|
||||
|
||||
|
||||
def lp_slug_for(campaign_type: str, phy_state: str | None = None) -> str:
|
||||
"""The order-page slug (== the discountable service slug) for a segment."""
|
||||
seg = DEFICIENCY_SEGMENTS.get(campaign_type)
|
||||
slug = seg["lp_slug"] if seg else "dot-full-compliance"
|
||||
if campaign_type == "state_weight_tax" and phy_state in _WEIGHT_TAX_LP:
|
||||
slug = _WEIGHT_TAX_LP[phy_state]
|
||||
if campaign_type == "state_emissions" and phy_state == "CA":
|
||||
slug = "ca-mcp-carb"
|
||||
return slug
|
||||
|
||||
|
||||
# ── Daily same-day coupon ───────────────────────────────────────────────────
|
||||
# Every send day gets ONE random 5-letter coupon at COUPON_PCT off, valid only
|
||||
# through 23:59:59 of the send date (America/New_York). The code is written to
|
||||
# the app's `discount_codes` table; the existing /api/v1/discount validator and
|
||||
# checkout enforce expiry + the service-fee-only scope (pass-through government
|
||||
# fees are never discounted). The code + prices are merged into the email so the
|
||||
# recipient sees a real, expiring deal.
|
||||
COUPON_PCT = int(os.getenv("CAMPAIGN_COUPON_PCT", "40"))
|
||||
# Eligible slugs = every discountable service a trucking campaign can link to.
|
||||
# Pass-through-only slugs (boc3-filing $25 passthrough, etc.) are intentionally
|
||||
# eligible too: the discount math only touches the service-fee portion, so a
|
||||
# code scoped to them simply yields $0 off the passthrough and full off the fee.
|
||||
COUPON_SLUGS = (
|
||||
"mcs150-update,usdot-reactivation,dot-drug-alcohol,dot-full-compliance,"
|
||||
"ucr-registration,state-trucking-bundle,intrastate-authority,irp-registration,"
|
||||
"ifta-application,hazmat-phmsa,state-emissions,state-weight-tax,trucking-wrap-up,"
|
||||
"boc3-filing"
|
||||
)
|
||||
_ET = timezone(timedelta(hours=-5)) # EST anchor; close enough for an end-of-day cutoff
|
||||
_COUPON_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ" # no I/O to avoid confusion
|
||||
|
||||
|
||||
def _random_coupon_code() -> str:
|
||||
import secrets
|
||||
return "".join(secrets.choice(_COUPON_ALPHABET) for _ in range(5))
|
||||
|
||||
|
||||
def get_or_create_daily_coupon(conn, send_date: date) -> str:
|
||||
"""Return the 5-letter coupon code for `send_date`, creating it if needed.
|
||||
|
||||
Idempotent: a marker in `description` (campaign-daily:<date>) lets a re-run
|
||||
on the same day reuse the existing code instead of minting a duplicate.
|
||||
"""
|
||||
marker = f"campaign-daily:{send_date.isoformat()}"
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT code FROM discount_codes WHERE description = %s LIMIT 1", (marker,))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return row[0]
|
||||
|
||||
# 23:59:59 ET of the send date
|
||||
expires = datetime.combine(send_date, datetime.min.time(), tzinfo=_ET) + timedelta(
|
||||
hours=23, minutes=59, seconds=59
|
||||
)
|
||||
starts = datetime.combine(send_date, datetime.min.time(), tzinfo=_ET)
|
||||
|
||||
# Retry on the (rare) code collision against the UNIQUE constraint.
|
||||
for _ in range(25):
|
||||
code = _random_coupon_code()
|
||||
try:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO discount_codes
|
||||
(code, description, discount_type, discount_value, applies_to,
|
||||
max_uses_per_email, active, starts_at, expires_at)
|
||||
VALUES (%s, %s, 'percent', %s, %s, 1, TRUE, %s, %s)
|
||||
ON CONFLICT (code) DO NOTHING
|
||||
RETURNING code
|
||||
""",
|
||||
(code, marker, COUPON_PCT, COUPON_SLUGS, starts, expires),
|
||||
)
|
||||
r = cur.fetchone()
|
||||
if r:
|
||||
conn.commit()
|
||||
LOG.info("[coupon] daily code %s (%d%% off, expires %s ET)",
|
||||
code, COUPON_PCT, expires.isoformat())
|
||||
return r[0]
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise RuntimeError("could not mint a unique daily coupon code")
|
||||
|
||||
|
||||
def coupon_attribs(coupon_code: str | None) -> dict:
|
||||
"""Merge fields for the same-day deal, blank when no coupon is active."""
|
||||
if not coupon_code:
|
||||
return {"coupon_code": "", "coupon_pct": "", "coupon_expires": ""}
|
||||
return {
|
||||
"coupon_code": coupon_code,
|
||||
"coupon_pct": str(COUPON_PCT),
|
||||
# Human-readable cutoff for the email body.
|
||||
"coupon_expires": "11:59 PM ET tonight",
|
||||
}
|
||||
|
||||
|
||||
def lp_link_with_coupon(campaign_type: str, phy_state: str | None,
|
||||
coupon_code: str | None) -> str:
|
||||
"""build_lp_link + a ?code= query param so the LP pre-applies the deal."""
|
||||
url = build_lp_link(campaign_type, phy_state)
|
||||
if coupon_code:
|
||||
sep = "&" if "?" in url else "?"
|
||||
url = f"{url}{sep}code={coupon_code}"
|
||||
return url
|
||||
|
||||
# ── TZ config: tz_key -> (states, send_hour_utc) ─────────────────────────────
|
||||
# 4AM EST = 09:00 UTC, each TZ +1hr so they get it at ~4AM local
|
||||
TIMEZONE_CONFIG = {
|
||||
|
|
@ -653,6 +758,15 @@ def run(send_date: date, dry_run: bool = False, preview: bool = False,
|
|||
warmup_cap: bool = True) -> None:
|
||||
conn = psycopg2.connect(DB_URL)
|
||||
|
||||
# Mint (or reuse) the same-day coupon for this send date so every campaign
|
||||
# in the run shares one expiring code. Preview/dry runs skip the write.
|
||||
daily_coupon = None
|
||||
if not dry_run and not preview:
|
||||
try:
|
||||
daily_coupon = get_or_create_daily_coupon(conn, send_date)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOG.warning("[coupon] could not mint daily coupon: %s (sending without)", exc)
|
||||
|
||||
base_mcs150 = get_base_campaign(CAMPAIGN_MCS150_ID)
|
||||
base_inactive = get_base_campaign(CAMPAIGN_INACTIVE_ID)
|
||||
|
||||
|
|
@ -788,7 +902,8 @@ def run(send_date: date, dry_run: bool = False, preview: bool = False,
|
|||
"email": TEST_EMAIL,
|
||||
"name": r0[2] or "Sample Carrier",
|
||||
"attribs": {"dot_number": r0[0], "company": r0[2] or "", "state": r0[3] or "",
|
||||
"lp_link": build_lp_link(campaign_type, r0[4])},
|
||||
"lp_link": lp_link_with_coupon(campaign_type, r0[4], daily_coupon),
|
||||
**coupon_attribs(daily_coupon)},
|
||||
}]
|
||||
else:
|
||||
subscribers = [
|
||||
|
|
@ -796,7 +911,8 @@ def run(send_date: date, dry_run: bool = False, preview: bool = False,
|
|||
"email": row[1],
|
||||
"name": row[2] or row[1],
|
||||
"attribs": {"dot_number": row[0], "company": row[2] or "", "state": row[3] or "",
|
||||
"lp_link": build_lp_link(campaign_type, row[4])},
|
||||
"lp_link": lp_link_with_coupon(campaign_type, row[4], daily_coupon),
|
||||
**coupon_attribs(daily_coupon)},
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue