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:
parent
329ef398cb
commit
ac10b4bd22
2 changed files with 19 additions and 6 deletions
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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> · expires 11:59 PM ET tonight`;
|
||||
deal.innerHTML = `<strong>${pct}</strong> with code <strong>${code}</strong> · limited-time offer`;
|
||||
deal.hidden = false;
|
||||
}
|
||||
} catch { /* no-op: fall back to full price */ }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue