Skip to content

feat(migrate): add the delivered site to a workspace whose plan covers it - #407

Merged
ABB65 merged 3 commits into
mainfrom
feat/migrate-bundle-states
Oct 4, 2026
Merged

ABB65 merged 3 commits into
mainfrom
feat/migrate-bundle-states

Conversation

@ABB65

@ABB65 ABB65 commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

What (W53: no locked button)

On /migrate/claim, a workspace that already pays no longer shows a disabled "Already subscribed" row.

  • Paid, active plan that covers the grant's plan and is not ending -> selectable, button "Add the site to this plan". New POST /api/migrate/grants/:grantId/attach (owner/admin only) ties the grant to the workspace and marks it used with no subscription: no trial, no second subscription, no Polar call.
  • Plan below the grant's -> row "Plan below Pro" + billing link when nothing else is selectable.
  • Ending (active + cancel_at_period_end) or trialing/unpaid -> 409 migrate.attach_no_plan; past_due -> 409 migrate.attach_past_due (fix billing first). Client hides the option, shows the reason on the row and the billing link.
  • Bundle, revoked, used and foreign-bound grants refused as in checkout.

Tests

tests/unit/migrate-grant-attach.test.ts (11, incl. ending -> 409 and past_due -> 409). eslint, typecheck, check:strings green.

Evidence

B2: waiting for staging screenshots (covers / too small / ending / past_due, next to the walkthrough copy). Merged on the tests; B2 stays open in the acceptance matrix until they exist.

…s it

W53: the claim screen locked every workspace that already paid, leaving the
customer with a disabled button. A workspace whose running plan covers the
grant's plan now offers "Add the site to this plan": POST
/api/migrate/grants/:grantId/attach ties the grant to the workspace and
marks it used with no subscription of its own, so no trial and no second
subscription start. A plan below the grant's shows why and links to billing
settings; a trialing or ending plan is refused with a reason.
@ABB65

ABB65 commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

t6 review @ 9920034 — NOT YET: 2 items, then I re-check the delta

Local evidence at this head: --project unit 182 files / 2007 tests, integration 52 / 487, nuxt 43 / 274 all pass; eslint on changed files and vue-tsc --noEmit clean. Studio CI: postgres-lineage pass, ci FAIL (see 1).

1. CI red: migrate.grant_revoked is not defined (check:strings). attach.post.ts uses it, but the key and the revoked_at column arrive with #406 (migration 045); this branch is cut from main without them. Land #406, merge main here, re-push; CI should go green. Until then revoked_at does not exist in the database this branch runs on.

2. "Ending" plans are not refused, though the docstring and PR body promise it. resolveBillingState returns subscribed for subscription_status='active' even when cancel_at_period_end = true, so a plan that ends this period passes billing.state === 'subscribed' and the grant is consumed on a workspace that is about to lose its plan. The screen also offers "Plan covers it" for it (paidPlanOf has the same gap). Fix: read cancel_at_period_end from the active account (already loaded inside resolveWorkspaceBilling; getActivePaymentAccount once more is fine) and answer 409 migrate.attach_no_plan on the server; hide the attach option on the client. Add the case to the test (it covers trialing and none, not ending, past_due or canceled).

Checked and fine: owner-only (getMigrateGrantForUser 404 for another user's grant) and owner/admin of the target workspace (403); coverage decided on the server from the account, never from the request (small plan, trial, none: 409); revoked / used / bundle / bound-elsewhere: 409; bindMigrateGrantWorkspace is the guard against grabbing a second workspace, a retry on the same workspace is harmless; no payment-provider call and no subscription or trial is created. past_due (inside its grace period) is allowed on both sides; say if that is intended.

(5) UI: the disabled "Already subscribed" row is gone for covering plans, copy matches the W53 text in the walkthrough; I have not seen a mock to compare, so a screenshot of the covered / too-small / upgrade states in the PR would close that line.

ABB65 added 2 commits October 3, 2026 20:40
t6 revise on #407: a plan set to end at the period's close (active +
cancel_at_period_end) answers 409 migrate.attach_no_plan, and a past_due
plan answers 409 migrate.attach_past_due (fix billing first). A grant is a
one-time credit; attaching it to a plan about to lapse would take the
site's Studio with it. The claim screen hides both options, shows the
reason on the row and, when nothing else is selectable, the billing link
(too small, ending and overdue each get their own hint).
@ABB65

ABB65 commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

t6 delta review @ 956b688 — ONAY

Both findings closed. (1) Main is merged; check:strings is green and the studio CI ci (15m28s) and postgres-lineage jobs pass. (2) The server refuses an ending plan (cancel_at_period_end on an active plan → 409 migrate.attach_no_plan) and a past_due one (409 migrate.attach_past_due); the screen hides both options and shows the reason with a billing link (billing_hint_ending|past_due|too_small, all keys present). New tests pin both refusals with no bind and no redeem.

Local evidence at this head: unit 181 files / 2001 tests, integration 52 / 487, nuxt 43 / 274 pass; eslint on changed files and vue-tsc --noEmit clean.

Still open, not blocking merge: the screenshots of the covered, too-small, ending and past-due states (staging), so B2 stays open until they are attached.

@ABB65
ABB65 merged commit c6206f5 into main Oct 4, 2026
2 checks passed
@ABB65
ABB65 deleted the feat/migrate-bundle-states branch October 4, 2026 08: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