Skip to content

feat(billing): honest yearly overage-lock copy, forms/comments/CDN over the billing window - #413

Merged
ABB65 merged 3 commits into
mainfrom
feat/polar-yearly-monthly-usage
Oct 4, 2026
Merged

ABB65 merged 3 commits into
mainfrom
feat/polar-yearly-monthly-usage

Conversation

@ABB65

@ABB65 ABB65 commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

What

Yearly plans only change billing frequency; limits stay the monthly tier limits. This PR makes the Studio side honest about that.

  1. Yearly overage-lock copy: a yearly workspace trying to enable overage now gets a yearly-specific message (yearly_plan reason) instead of the trial/not-in-subscription one.
  2. Forms, comments and CDN count over the billing window (separate commit, droppable): they now follow the same slice as AI/API/MCP/media, and yearly plans slice into monthly windows.

Upgrade notes

  • Migration 046_usage_window_quotas.sql drops and recreates create_form_submission_if_allowed / create_comment_if_allowed with two defaulted window params. Run it before the new image. The old image still works against the new functions (params default), so rolling back the image alone is safe.
  • No NOTIFY pgrst added: earlier migrations that changed RPC signatures (010, 038) don't use it either, and none in the repo does. Supabase-pair reviewer, please confirm.
  • Subscribed workspaces' form/comment/CDN usage-alert keys re-key from YYYY-MM to the slice start once, so one extra usage alert may go out in the transition month. CDN window is whole UTC days.

Deploy order (migration 046 before the image)

  • Managed + postgres pair (Railway): automatic. The Railway Pre-Deploy Command node scripts/migrate-postgres.mjs runs before the new image serves; the app does not migrate at boot (Dockerfile comment, docs/REMOTE_MCP_SUBMISSION.md). Re-runs skip applied files via public.schema_migrations.
  • Supabase pair: manual. Apply with the Supabase CLI (pnpm db:migrate, docs/DEPLOYMENT.md) before the image goes out.
  • Merging to main deploys staging, so on the managed pair 046 applies by itself as part of that deploy.

Note for the companion-subscription PR

isYearlyPeriod labels every unpriced meter on a >35-day period as yearly_plan. Correct today (yearly products price nothing). Once the companion usage subscription merges billable_meters, a yearly account missing one meter should read not_in_subscription again, so the reason picker must key on "subscription prices nothing", not "period is long". Label only, not an enforcement bug.

Verification

  • pnpm test:ci: unit 2049, integration 488, nuxt 274, all green
  • Contract suite: 162 passed against a throwaway Postgres (migration 046 applied via scripts/migrate-postgres.mjs)
  • pnpm typecheck clean, eslint 0 errors

ABB65 added 3 commits October 4, 2026 23:00
…act support'

A yearly subscription prices no meter, so its overage toggles are locked. The lock
reason read 'not available on your subscription yet — contact support to update it',
which support cannot do: Polar invoices metered usage on the subscription's own
cycle, so overage on a yearly plan would be billed once a year, not monthly.
The lock now carries its own reason (yearly_plan, a period longer than a month) and
the usage panel and the overage-settings refusal say what is true: usage resets
every month and stops at the plan limit until overage is billed monthly there.
… like the other quotas

AI, API and MCP credits already reset on the subscription's anniversary; forms,
comments and CDN origin transfer still counted the calendar month. The payment
provider invoices overage on the subscription's cycle, so the screen and the invoice
could close on different days — and on a yearly plan, whose usage is monthly, the
counters must follow the monthly slice too.

- the row-counted readers and the two atomic submit functions take a window
  [from, to); without one they keep the calendar month (free, self-hosted)
- migration 046 replaces create_form_submission_if_allowed and
  create_comment_if_allowed with the same bodies plus two defaulted window
  parameters; a caller that passes none behaves as before
- the CDN keeps one row per UTC day, so its window is whole days: the day the slice
  opens on is in, the day the next one opens on is out; its origin budget counter is
  keyed by the window and Retry-After runs to the slice end
- the public form/comment/CDN routes resolve the window from a one-minute cache of
  the payment account (the window itself is computed at each call's time)

A subscribed workspace's form/comment/CDN usage alerts re-key from YYYY-MM to the
slice start once, so an alert already sent this month can be sent once more.
@ABB65

ABB65 commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

ONAY @ 8a041b8 — ORK review: yearly overage-lock copy + forms/comments/CDN counters aligned to the billing slice (founder approved 2026-10-04). 046 re-creates the two RPCs with defaulted window params (old callers keep calendar month); no grant changes before/after. CI ci+postgres-lineage green; studio-dev local test:ci (unit 2049, integration 488, nuxt 274), contract 162.

@ABB65
ABB65 merged commit e32c6a7 into main Oct 4, 2026
2 checks passed
@ABB65
ABB65 deleted the feat/polar-yearly-monthly-usage branch October 4, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant