/* ============================================================ 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 };