Repository navigation
WP-08: Billing + quota - #7
Merged
Merged
Conversation
Entitlements (free 2/mo <=60min footer, pro 10/mo <=2h, founding = pro yearly), quota month + wall copy (verbatim §6), LS checkout/cancel/webhook behind the documented interface (HMAC port of api/ls-webhook.js, webhook_events idempotency, event mapping per §7.1), owner-only manual plan flag, derived founding counter (first 100), MK-9 billing settings page. Tests-first: entitlements, webhook HMAC/idempotency/mapping, service, LS client, e2e. LS keys do not exist yet (owner blocker, docs/FOR-ABID.md §7) — everything verified against signed fixtures + the mocked single LS fetch boundary.
# Conflicts: # docs/relaunch/DECISIONS.md
# Conflicts: # docs/relaunch/DECISIONS.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
OWNER BLOCKERS (top) — Lemon Squeezy does not exist yet
No
LS_API_KEY/LS_STORE_ID/LS_WEBHOOK_SECRET/ variant ids / Founding payment link exist anywhere (docs/FOR-ABID.md§1). Until the owner runs the activation steps (now written up in docs/FOR-ABID.md §7, new):POST /api/billing/checkout→ 503 "Checkout isn't set up in this preview."POST /api/webhooks/lemonsqueezy→ 503 (fails closed; never accepts unsigned traffic)LS_FOUNDING_PAYMENT_URL+ owner flag in/admin) works with zero LS config apart from that one env varU14's "plan becomes Pro within 10 s of webhook" could not be verified against a live LS store. Everything else is verified below.
What shipped
web/lib/billing/entitlements.ts— plan limits (Free 2/mo ≤60 min + footer; Pro 10/mo ≤2 h, no footer; Founding = Pro yearly),quotaMonth(UTC, char(7) per §7.1), reset date,effectivePlan(cancel keeps Pro untilcurrent_period_end, then derives Free — no cron), verbatim quota-wall + cancel-dialog copy,lsStatusToWorkspaceStatus, founding counter math (first 100, derived only)assertAndConsumeQuota(counted when generation starts) +consumeEpisodeQuota/refundEpisode(refunded on pipeline failure) for WP-05/06 to call around POST /api/episodesweb/lib/billing/ls.tssingle fetch boundary; shapes verified against docs.lemonsqueezy.com today) —POST /api/billing/checkout(JSON:API checkout, variant fromLS_VARIANT_PRO_MONTHLY/LS_VARIANT_FOUNDING,checkout_data.custom.workspace_id→meta.custom_data),POST /api/billing/cancel(DELETE /v1/subscriptions/:id),POST /api/webhooks/lemonsqueezy(HMAC-SHA256 X-Signature viacrypto.timingSafeEqual, ported fromapi/ls-webhook.jsincl. serialization fallbacks; idempotent viawebhook_eventskeyed sha256(raw body))current_period_end(renews_at)/founding;subscription_cancelled→ status=cancelled, period end = ends_at (U15 access date);subscription_expired→ downgrade; payment_failed/paused → past_due; unknown workspace → 200 unmatched (logged, still deduped); unhandled events ignoredPOST /api/admin/billing,OWNER_EMAILScheck inline inlib/billing/service.ts(fail-closed 403 when unset) because WP-04'srequireOwneris concurrent — tie-break logged, integrator may unifyCOUNT(*)of founding workspaces, "N of 100 left" on the button, hidden (not faked) without a DB/app/settings/billing— plan card (name/price/renewal date), QuotaMeter ("3 of 10 episodes · resets Nov 14"), Upgrade/$19 buttons, Founding button, [Manage payment] via the signed LSurls.customer_portal(fetched at render, hidden when unavailable), Cancel → dialog "Cancel Pro? You'll keep Pro until {date}. Your episodes stay." [Keep Pro] [Cancel plan]components/app/billing/quota-wall.tsx) with the exact §6 copy, ready for WP-06 intake mountingLS_*+OWNER_EMAILSadded toweb/.env.example(empty); 13 decisions appended todocs/relaunch/DECISIONS.mdAcceptance (U14/U15 in LS TEST MODE semantics)
Verified (automated):
subscription_createdfixture → workspace updateplan=pro, plan_source=ls, current_period_end=renews_at(+founding variant) — 17 webhook tests incl. idempotent redelivery (duplicate no-op, side effect once) and full event mappingpnpm lint✅ ·pnpm typecheck✅ ·pnpm test232 passed (19 files) ✅ ·pnpm build✅ ·pnpm e2e20 passed ✅Not verifiable without owner LS setup (stated explicitly):
httpLsClientfetch modulePreview: https://episodekit-1yc5wczlg-pera-byte.vercel.app/app/settings/billing (keyless shell: Free card, meter, upgrade buttons; server probed: webhook 503, checkout 503 preview copy)
Notes for integrator: WP-02's
settings.spec.tsbilling-stub assertion updated (stub replaced by the real page — the stub's own comment said it ships in WP-08).db/queries.tsgains billing functions (documented boundary). Billing page is partial-prerender + Suspense (Next 16cacheComponentsforbidsdynamic = "force-dynamic"). Local e2e teardown of the detachednext startwebServer hangs on this machine (WP-00 decision note) — tests were run against a reused server; CI starts its own.