Skip to content

feat(migrate): end of a prepaid Studio year, "Keep Studio" renewal - #545

Merged
ABB65 merged 5 commits into
mainfrom
t7/bundle-prepaid-year-end
Nov 4, 2026
Merged

ABB65 merged 5 commits into
mainfrom
t7/bundle-prepaid-year-end

Conversation

@ABB65

@ABB65 ABB65 commented Nov 4, 2026

Copy link
Copy Markdown
Member

Stacked on #542 (prepaid Studio year core). Review that first; this PR's base is its branch and moves to main once it merges.

What this adds

Billing page and banner

  • "Prepaid through {date}" replaces the monthly price during a prepaid year.
  • From 30 days before the end: "Keep Studio" on Starter or Pro yearly (owner/admin; members see "ask the owner"). Before that, the date it opens. After choosing, the scheduled plan, date and price, plus Manage Subscription.
  • The trial banner says when the prepaid year ends and links to Keep Studio, only inside the window and only while no renewal is set.

Keep Studio (POST /api/billing/keep-studio, server/utils/migrate-prepaid-renewal.ts)

  • Yearly checkout with a trial that ends with the prepaid year (clamp(ceil(days left), 1, 30)), so nothing is charged before the year ends.
  • Metadata migrate_renewal_grant_id, never migrate_grant_id: the webhook links it to the grant (renewal_subscription_id), no redeem, no duplicate check. trial_origin: 'prepaid_renewal' is not a trial for trial context or trial reminders.
  • Refusals: prepaid_not_active, renewal_not_open, renewal_scheduled. An ordinary checkout during a prepaid year now answers with the prepaid message.
  • Polar: yearly product per plan. Stripe: interval: 'year' is refused (not configured there).

Year-end sweep (server/plugins/migrate-prepaid-sweep.ts, every 6 h)

  • Mails at T−30, T−7 and the end; forward-only cursor migrate_grants.prepaid_reminder_stage (migration 056, CHECK 0–3).
  • A year with a renewal gets no "choose a plan" mail. At the end the year comes off through endPrepaidYear, then the ended mail.
  • New templates: prepaid-ending, prepaid-ended, prepaid-renewed.

Honest copy

  • Overage stays locked during the year with its own reason prepaid (settings GET/PATCH, usage, usage panel).
  • The limit-reached text, the usage-alert next step and the lock text say the year is prepaid instead of "change plan in Billing".
  • Usage alerts read prepaid_plan / prepaid_until (F2).
  • No "now on the free plan" canceled mail while the prepaid year runs; at renewal conversion the activation mail is the yearly prepaid-renewed mail.
  • The usage period of a renewal trial stays the calendar month, so the free quota is not reset by the trial's billing key.

Revoke residual

  • Revoke cancels a renewal with the year.
  • A repeated revoke takes off a year an earlier call could not.

Attach

  • In the last 30 days of a prepaid year with no renewal, attach is refused (attach_prepaid_ending).
  • With a renewal, the renewal plan must cover the grant.

Claim page

  • A prepaid grant says "{plan}, prepaid through {date}". No cancel note: prepaid grants are bundles.

Docs

  • docs/PAYMENT_PROVIDERS.md: "Prepaid Studio year".

Known edges (not in this PR)

  • Keep Studio opens its usage companion subscription at checkout, a month or less before the year ends. It bills nothing while overage is locked.
  • Cancelling a renewal before the year ends sends the generic "cancel scheduled" mail wording.
  • If the stage write fails after the year is taken off, the ended mail for that year is not retried.

Tests

  • Unit: renewal checkout, sweep, attach, trial cap, usage period, usage alerts, email templates, checkout trial days, revoke route.
  • Integration: webhook (renewal link, cancel during the year, yearly activation mail), overage settings.
  • Contract: prepaid grants (renewal, cursor, window), usage alerts with a prepaid workspace.
  • Nuxt: the trial banner prepaid window.

ABB65 added 4 commits November 4, 2026 13:47
Provision with billing.collector migrate reserves the grant with no Polar call.
POST /api/migrate/grants/activate starts a prepaid year from the payment.
Billing state prepaid wins while prepaid_until is ahead; overage is locked during it.
Revoke ends the prepaid year. Migration 055 is additive (045 revoked CHECK widened).
Revoke read the grant before an activate wrote the prepaid year, so it skipped ending it and
left a refunded year live. It now also ends the year on the fresh row the revoke mark returns.
A prepaid Studio year (paid with the Migrate order) now has an end the
owner can see and act on:

- Billing shows "Prepaid through {date}" instead of a monthly price; from
  30 days before the end it offers "Keep Studio" on Starter or Pro yearly.
  The yearly checkout carries a trial that ends with the prepaid year, so
  nothing is charged before it ends. It is tagged migrate_renewal_grant_id,
  never treated as a trial or a grant redeem.
- A sweep every 6 h mails the owner at T-30, T-7 and the end (cursor in
  migrate_grants.prepaid_reminder_stage, migration 056) and ends the year
  through endPrepaidYear. A year with a renewal gets no "choose a plan"
  mail.
- Overage stays locked during the year with its own reason and copy; the
  limit-reached text, the usage alert next step and the banner say so.
- Usage alerts read the prepaid plan and date (prepaid_plan, prepaid_until).
- The canceled mail is not sent while the prepaid year still runs; the
  activation mail at renewal conversion is the yearly "prepaid-renewed"
  mail.
- The usage period of a renewal trial stays the calendar month.
- Attach refuses a grant in the last 30 days of a prepaid year with no
  renewal; with a renewal the renewal plan must cover the grant.
- Revoke cancels a "Keep Studio" renewal with the year, and a repeated
  revoke takes off a year an earlier call could not.
- The claim page states the prepaid year and date for a prepaid grant.
- docs/PAYMENT_PROVIDERS.md: the prepaid year section.
… mail with the button

- A plain subscription that reuses the account row a "Keep Studio" renewal wrote drops its
  `prepaid_renewal` trial origin on created/updated, so its activation is not told as the yearly
  renewal.
- The T-30 window ends at 30 days, when "Keep Studio" opens, not 30.5: the mail's button works when
  it arrives.
Base automatically changed from t7/bundle-prepaid-core to main November 4, 2026 13:34
#542 landed as a squash (89f30db); the branch carried its commits. Conflicts resolved as
this branch's changes on top of main (3-way with #542's head as base), keeping #541/#543/#544.
@ABB65
ABB65 merged commit c1a0036 into main Nov 4, 2026
2 checks passed
@ABB65
ABB65 deleted the t7/bundle-prepaid-year-end branch November 4, 2026 13:51
ABB65 added a commit that referenced this pull request Nov 4, 2026
Prepaid Studio year when Migrate collects the bundle (#542) and its year end with
Keep Studio renewal (#545). Migrations 055 and 056; no new env vars or flag.
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