Skip to content

feat(migrate): provision a covered order onto the running plan - #409

Merged
ABB65 merged 6 commits into
mainfrom
feat/migrate-provision-covers
Oct 4, 2026
Merged

ABB65 merged 6 commits into
mainfrom
feat/migrate-provision-covers

Conversation

@ABB65

@ABB65 ABB65 commented Oct 4, 2026

Copy link
Copy Markdown
Member

What (W54 covers)

When the GitHub account already runs a Studio plan at least the sized one, POST /api/migrate/provision ties the grant to that plan's workspace (the personal one first) and answers state: 'redeemed' with the workspace and no checkout.

  • Studio fee $0: the quote must equal Migrate's own fee (else 409 migrate.quote_changed); Polar is not called, no checkout saved (tests pin both).
  • Grant gets no subscription of its own; a later revoke cancels nothing of the customer's plan.
  • Repeat provision is idempotent and keeps the workspace the grant is already tied to; a grant opened with a checkout, a revoked one or a foreign one is refused.
  • too_small still answers 409 migrate.bundle_state_unsupported (the real upgrade is a separate PR after refund/revoke and the staging E2E).
  • @contentrain/types 1.47.0 (union response). A checkout answer is never redeemed: a grant paid in the same moment is refused with 409 migrate.grant_used.
  • New coveringWorkspace() in migrate-account-state.ts.

Tests

Provision (covers: bound + redeemed + no Polar, quote, idempotent, re-quote/refusals; same-moment paid), coveringWorkspace. Unit + integration, eslint, typecheck green; main CI green before push.

ABB65 added 3 commits October 3, 2026 20:31
W54 covers: when the GitHub account already runs a plan at least the sized
one, provision ties the grant to that plan's workspace (the personal one
first) and answers redeemed with no checkout. The Studio fee is $0, the quote
must equal the Migrate fee, and Polar is not called. A repeat keeps the
workspace the grant is tied to. too_small still answers 409
migrate.bundle_state_unsupported until the upgrade flow exists.

Needs @contentrain/types 1.47.0 (provision answer without a checkout).
…emed

Provision answers are a union now: a checkout answer is never redeemed, so a
grant paid in the same moment is refused with 409 migrate.grant_used.
@ABB65

ABB65 commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

t6 review @ 2e55fe1 — ONAY for what was asked; one money question (non-blocking) for t8/ORK

Evidence at this head: studio CI ci (13m35s) and postgres-lineage pass. Local: unit 183 files / 2031 tests, integration 52 / 487, nuxt 43 / 274 pass; eslint on changed files and vue-tsc --noEmit clean.

Checked, as asked:

  • No Polar call, no checkout record: the covered path returns before bundleWorkspace/getActivePaymentAccount/checkout; tests pin createBundleCheckout, saveMigrateGrantCheckout and getActivePaymentAccount not called.
  • quote == Migrate fee: migrate_fee_cents + year1_cents !== quoted_total_cents → 409 quote_changed; a covered account has year1_cents 0, so only the bare Migrate fee agrees (test "agrees only a quote equal to the Migrate fee", and the response is validated against the quote).
  • Idempotent: a repeat keeps the workspace already tied to the grant (grant.workspace_id wins over the freshly chosen one), bind… refuses a different one (409 grant_bound_elsewhere), redeem is not marked twice.
  • Paid in the same moment: the checkout path now refuses bound.redeemed_at with 409 grant_used and the checkout answer can no longer say redeemed; a grant opened with a checkout and still unpaid is never reused as covered (409 quote_changed); revoked and foreign grants refused.
  • too_small stays 409 bundle_state_unsupported, with no claim and no checkout (test).
  • Workspace choice: candidates come only from listOwnedWorkspacesAdmin (owner, so admin is implied) filtered by planCovers(plan, needed), personal first. A grant cannot be bound to a workspace that does not cover or that the caller does not own. A revoke later cancels nothing (no subscription bound).

Question, not a blocker here: RUNNING_STATES is subscribed | past_due | canceled, and subscribed includes an active plan with cancel_at_period_end. For pricing that was already so; with this PR it also decides binding and a $0 Studio line, so a customer on a plan that is ending (or overdue) pays only Migrate's fee and the Studio they were told is included ends within days. #407 refuses exactly these states for attach. If the same rule is wanted here, count only active and not-ending in runningPlans (then those accounts quote as none and take the normal checkout). I am not asking for it in this PR unless ORK/the founder wants the covered path to match attach; say which.

ABB65 added 3 commits October 4, 2026 12:22
Only an active plan that is not scheduled to end covers a Migrate order.
Ending, past_due and canceled plans fall to none and pay the normal bundle,
as attach already does. The bundle workspace now skips any workspace that
still holds a subscription, so a second one is never opened on a plan that
is ending; when every owned workspace holds one, provision refuses with
billing.subscription_exists.
Provision answers 409 with data.code subscription_exists when every owned
workspace already holds a subscription, so Migrate can show the customer the
way out without matching localised text.
…esume

The 409 subscription_exists answer carries workspace_slug, the personal
workspace first, so Migrate can link to that workspace billing page. Only a
workspace the caller owns, and only in the S2S answer.
@ABB65

ABB65 commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

ONAY — f439741 (delta d5acb56..f439741, t6 review)

Checked at f439741:

  • data.code = 'subscription_exists' is a stable, non-localized handle; the message stays localized text. Only set on the no-free-workspace refusal.
  • data.workspace_slug is the slug of the first of the caller's OWN owned workspaces (listOwnedWorkspacesAdmin(user.id), personal first), and only on the signed S2S POST /api/migrate/provision (no session surface). Omitted when the workspace row is missing.
  • runningPlans rule + skipping workspaces that already hold a subscription unchanged; claimMigrateGrant is never reached on this path (test asserts it), so there is never a second subscription.
  • 4xx refusal does not release the jti (an answer, not our failure).

Local: unit 2035 passed, integration 487 passed, nuxt 274 passed, eslint and nuxt typecheck clean.

@ABB65
ABB65 merged commit 29904a1 into main Oct 4, 2026
2 checks passed
@ABB65
ABB65 deleted the feat/migrate-provision-covers branch October 4, 2026 10:10
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