Wave 1: kill three trust bugs (billing renewal + namesake collision + Desk copy)
FIX 1 — Honest billing renewal render. VYNDR tiers are monthly, so a `subscription_end` far in the future (the manually-seeded "RENEWS 6/9/2036" founder row) is a comped/lifetime/seed value, not a renewal. New web/src/lib/billingDisplay.js `classifyRenewal()` → date | none | lapsed | unknown (strict Date.parse guard, MONTHLY_RENEWAL_MAX_DAYS=60). Profile page renders the classified label for both the "Renews" stat and the cancel-scheduled "Access ends" line — no raw far-future date. No DB row mutated. FIX 2 — MLB namesake collision (James Wood → "Chicago Cubs"). searchPlayer now collects ALL exact-nameKey matches instead of first-`.find`; a ≥2 collision resolves ONLY via a confident teamHint (the prop's game participants, matched against the cached /teams list with ESPN↔statsapi abbr reconciliation), else refuses (null) — never guesses. The hint threads getPlayerStats → resolvePlayerStats → snapshotService (built from each prop's home/away team). Join invariant: a single-exact player whose team isn't in the hinted game has its team DROPPED (null), so streaks/rosterlogs never tag a foreign team. Full teamHint recovery shipped (not just the refuse fallback). FIX 3 — DeskShowcase headline "A $1M terminal." → deadpan value-showing copy "Every grade, every alt line, live." Prices ($44.99 / $34.99) unchanged. Tests: billingDisplay.test.js (7), mlbNamesakeResolve.test.js (12, disambiguation + join invariant + pure helpers), ds5PricingStates updated to assert the new headline and no "$1M". Full suite green (237 suites / 2863 tests); web `next build` exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/* ============================================================
|
||||
VYNDR — BILLING DISPLAY (honest renewal render).
|
||||
Plain CommonJS so .tsx components import it AND the Jest suite
|
||||
requires it directly (same pattern as colorContract.js / checkout.js).
|
||||
|
||||
The lie this kills: the profile page rendered `subscription_end`
|
||||
verbatim, so a manually-seeded / comped founder row reading
|
||||
"6/9/2036" showed as a real renewal. VYNDR tiers are MONTHLY only
|
||||
(no annual), so any date more than ~60 days out is NOT a plausible
|
||||
monthly next-bill — it is a comped / lifetime / seed value and must
|
||||
NOT be rendered as a renewal date.
|
||||
|
||||
Doctrine: absent-but-honest beats wrong-but-full. Never render a
|
||||
renewal date the billing cadence can't justify. Strict parsing
|
||||
(the `Number(null) === 0` class of bug) — an unparseable value is
|
||||
`unknown`, never coerced to an epoch date.
|
||||
============================================================ */
|
||||
|
||||
// A monthly plan renews ~30 days out; allow slack for proration / grace,
|
||||
// but a value beyond this many days out cannot be a monthly renewal.
|
||||
const MONTHLY_RENEWAL_MAX_DAYS = 60;
|
||||
|
||||
// A renewal more than this many days in the PAST is a lapsed subscription
|
||||
// (a small grace window absorbs clock skew / just-past renewals).
|
||||
const LAPSED_GRACE_DAYS = 2;
|
||||
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
/**
|
||||
* classifyRenewal(subscriptionEnd, nowMs) → { kind, iso? }
|
||||
*
|
||||
* kind ∈
|
||||
* 'date' — a plausible monthly renewal (0..~60d out). Carries `iso`
|
||||
* (the parsed timestamp) for the caller to localize.
|
||||
* 'none' — more than ~60d out → comped / lifetime / seed value; there
|
||||
* is no scheduled monthly renewal to show.
|
||||
* 'lapsed' — more than 2d past → the subscription window has ended.
|
||||
* 'unknown' — absent / empty / unparseable → show an em dash.
|
||||
*
|
||||
* @param {string|number|null|undefined} subscriptionEnd provider-asserted end.
|
||||
* @param {number} [nowMs] current epoch ms (injectable for tests).
|
||||
*/
|
||||
function classifyRenewal(subscriptionEnd, nowMs = Date.now()) {
|
||||
if (subscriptionEnd === null || subscriptionEnd === undefined || subscriptionEnd === '') {
|
||||
return { kind: 'unknown' };
|
||||
}
|
||||
const t = Date.parse(subscriptionEnd);
|
||||
if (Number.isNaN(t)) return { kind: 'unknown' };
|
||||
|
||||
const now = Number.isFinite(nowMs) ? nowMs : Date.now();
|
||||
const diffDays = (t - now) / DAY_MS;
|
||||
|
||||
if (diffDays < -LAPSED_GRACE_DAYS) return { kind: 'lapsed' };
|
||||
if (diffDays > MONTHLY_RENEWAL_MAX_DAYS) return { kind: 'none' };
|
||||
return { kind: 'date', iso: new Date(t).toISOString() };
|
||||
}
|
||||
|
||||
module.exports = { classifyRenewal, MONTHLY_RENEWAL_MAX_DAYS };
|
||||
Reference in New Issue
Block a user