fix(coupon A/B): discount banner never displayed + same-day expiry killed the test

Two bugs that invalidated the price A/B/C test (20/30/0):

1. OrderPriceBanner.astro shipped with data-api="" (PUBLIC_API_URL was never
   set at build), so the ?code= discount lookup hit a relative /api/... path on
   the static host -> 502. The struck-through discounted price NEVER rendered
   for anyone who clicked a coupon link; every visitor saw full price. Fixed to
   fall back to window.__PW_API (set in Base.astro) then the prod API host, the
   same pattern the rest of the site uses.

2. Daily coupons expired 23:59 ET of the SEND day. The builder runs in the
   morning ET and carriers read mail across ~36h, so most discount arms saw an
   already-expired code by open time. Extended to 23:59 ET of the day AFTER
   send (~1.5 days), and updated the email + banner wording to match.

Net: the A/B/C arms can now actually be redeemed, so the test can produce a
real signal. Measured by paid Stripe orders keyed to the arm (re-hash of the
converter email), not bot-polluted opens/clicks.
This commit is contained in:
justin 2026-06-30 16:21:52 -05:00
parent 329ef398cb
commit ac10b4bd22
2 changed files with 19 additions and 6 deletions

View file

@ -426,9 +426,14 @@ def get_or_create_daily_coupon(conn, send_date: date, pct: int | None = None) ->
if row:
return row[0]
# 23:59:59 ET of the send date
# 23:59:59 ET of the day AFTER the send date. A same-day-only coupon was
# effectively dead by the time most carriers opened the email (the daily
# builder runs in the morning, ET, and people read mail across the next
# ~36h), which invalidated the price A/B test — most discount arms saw an
# expired code. Giving ~1.5 days means the deal is live when the email is
# actually read.
expires = datetime.combine(send_date, datetime.min.time(), tzinfo=_ET) + timedelta(
hours=23, minutes=59, seconds=59
days=1, hours=23, minutes=59, seconds=59
)
starts = datetime.combine(send_date, datetime.min.time(), tzinfo=_ET)
@ -515,8 +520,9 @@ def coupon_attribs(coupon_code: str | None, coupon_pct: str | None = None) -> di
return {
"coupon_code": coupon_code,
"coupon_pct": coupon_pct or str(COUPON_PCT),
# Human-readable cutoff for the email body.
"coupon_expires": "11:59 PM ET tonight",
# Human-readable cutoff for the email body. Matches the mint window
# (end of the day AFTER the send), so the wording stays truthful.
"coupon_expires": "11:59 PM ET tomorrow",
}

View file

@ -52,7 +52,14 @@ const API = import.meta.env.PUBLIC_API_URL || "";
const priceCents = parseInt(banner.dataset.priceCents || "0", 10);
const slug = banner.dataset.serviceSlug || "";
const api = banner.dataset.api || "";
// Resolve the API base the same way the rest of the site does. The build-time
// PUBLIC_API_URL was never set, so data-api shipped empty and the banner hit a
// relative /api/... path on the static host (502) -- meaning the discount NEVER
// displayed for anyone who clicked a coupon link. Fall back to window.__PW_API
// (set in Base.astro) and finally the prod API host.
const api = banner.dataset.api
|| (window as any).__PW_API
|| "https://api.performancewest.net";
if (!priceCents) return;
try {
@ -73,7 +80,7 @@ const API = import.meta.env.PUBLIC_API_URL || "";
if (nowEl) nowEl.textContent = fmt(now);
if (deal) {
const pct = d.discount_type === "percent" ? `${d.discount_value}% off` : `${fmt(d.discount_cents)} off`;
deal.innerHTML = `<strong>${pct}</strong> with code <strong>${code}</strong> &middot; expires 11:59 PM ET tonight`;
deal.innerHTML = `<strong>${pct}</strong> with code <strong>${code}</strong> &middot; limited-time offer`;
deal.hidden = false;
}
} catch { /* no-op: fall back to full price */ }