Skip to content

feat(migrate): provision the Studio+Migrate bundle (S2) - #404

Merged
ABB65 merged 5 commits into
mainfrom
feat/migrate-provision-s2
Oct 3, 2026
Merged

ABB65 merged 5 commits into
mainfrom
feat/migrate-provision-s2

Conversation

@ABB65

@ABB65 ABB65 commented Oct 3, 2026

Copy link
Copy Markdown
Member

S2 of the Migrate-with-Studio bundle (founder-approved W48-b).

What

  • POST /api/migrate/provision (signed claim v2, purpose provision, jti single-use): ensures the Studio user for the GitHub account, opens a bundle grant (migration 044) and a Polar checkout at the quoted total (Migrate fee + Studio year 1 at 20% off) on a bundle product. Repo-less claims are valid (types 1.44.0).
  • Only account state none is provisioned; covers/too_small → 409 migrate.bundle_state_unsupported (S3). Live subscription → 409.
  • On subscription.created/updated the subscription moves to the yearly list product effective next period (sandbox-verified); a reconciler (every 6h) retries and alarms when renewal is ≤30 days away.
  • account-state answers carry renewal_cents (yearly list price; 0 when covers) as an extra key until types 1.45.0 (ai#413).
  • AuthProvider ensureUserForProviderAccount in both implementations; Polar plugin createBundleCheckout/moveBundleSubscriptionToList (Stripe stubs throw); trial checkout/claim refuse bundle grants.
  • Config: four new product-id env names (see docs/PAYMENT_PROVIDERS.md). Not set in prod; this ships with v0.4.8 only after founder approval.

Not in this PR

polar-sync yearly/bundle products (waiting for the catalog answer), grants revoke/refund (S3), trial removal.

Checks (local)

lint 0 errors, typecheck 0, unit 1991, integration 486, nuxt 274, contract (postgres:16) 158 pass / 1 skip.

ABB65 added 5 commits October 3, 2026 17:18
… jti back on our own failure

Supabase pair: public.migrate_user_id_by_identity reads auth.identities in one
indexed lookup (a GitHub linked later is found). Managed pair: auth.identities
for the plain-PG lineage, recorded at every OAuth sign-in, so a later Google
sign-in no longer hides the GitHub id. account-state releases the jti when its
own work fails, so Migrate's retry of the same request is taken.
…r checkout

POST /api/migrate/provision takes a signed claim v2, finds or creates the
Studio account by GitHub id, records one bundle grant per order and opens
one Polar checkout at the quoted total.

The webhook then moves the subscription to the yearly list product at the
next period; a scheduled reconciler retries a failed move and logs an alarm
30 days before renewal. Migration 044 makes trial days and repo optional
for bundle grants.
Pin @contentrain/types 1.44.0 (claim v2 repo optional); keep the bundle
grant methods beside the order lookup from main; add a repo-less
provision route test.
The cart fine print needs the yearly list price the subscription renews
at; Migrate must not compute it. 0 when nothing is added (covers).
@ABB65

ABB65 commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

ONAY (t6) — a0af19d

Doğrulama (yerel + CI):

  • Migrasyon, iki koşucu: postgres:16 (docker) üzerinde scripts/migrate-postgres.mjs → 46 uygulandı; ikinci koşu 0 uygulandı / 46 atlandı (idempotent). Sıralama: 043_managed_auth_identities < 043_migrate_identity_lookup (kimlik tablosu fonksiyondan önce). verify-managed-schema geçti (auth.identities listede). Supabase koşucusu: CI ci işi (supabase start) yeşil, 16m31s.
  • Contract: pnpm test:contract 27 dosya / 158 test geçti (1 atlama eski: WP fixture env'i yok); test:runtime 1/1; test:rls 4/4 — hepsi postgres:16'ya karşı. Yeni: migrate-grants + managed-auth contract.
  • Yerel: eslint 0 hata, vue-tsc temiz; unit 181 dosya / 1991; integration 52 / 486; nuxt 43 / 274. Hedefli: provision/bundle-subscription/bundle-polar/account-state/grant-routes 71/71.
  • Webhook idempotency: redeemMigrateGrant → markMigrateGrantRedeemed idempotent; taşıma applyBundleListProduct hiç fırlatmaz (hata = grant pending, webhook 200), Polar tarafı moveBundleSubscriptionToList zaten-listede/bekleyen güncellemeyi tekrar göndermez, markMigrateBundleApplied iki koşucuda da tek yazım (coalesce / is null). created/updated hangisi önce gelirse aynı sonuç.
  • Reconciler: 6 saatte bir, bekleyen yoksa sağlayıcıya gitmez, birden çok replika güvenli (idempotent); [migrate-bundle] ALARM yenilemeye ≤30 gün ya da tarih bilinmiyorsa error düzeyinde.

Bloklamayan bulgular (sonraki iş):

  1. Supabase ensureUserForProviderAccount, e-postası olan mevcut kullanıcıyı kimlik çakışması denetlemeden döndürür; postgres kolu farklı GitHub hesabında IdentityConflictError verir. İki kol aynı davranmalı; contract testi yalnız postgres'te koşuyor.
  2. Tutar değişince (quote_changed sonrası yeniden teklif) yeni checkout açılır, eskisi Polar'da süresi bitene dek ödenebilir kalır → eski tutarla ödeme mümkün. Eski checkout'u kapatma/süre kısaltma ya da webhook'ta amount ≠ grant.amount_cents uyarısı gerekir.
  3. createBundleCheckout başarılı, saveMigrateGrantCheckout düşerse checkout yetim kalır (yorum 'kalıcı' diyor); yeniden denemede ikincisi açılır (10 sn oran sınırı sonrası).
  4. Reconciler yalnız redeemed_at dolu satırlara bakar: ödeme yapılmış ama webhook hiç gelmediyse (redeemed null) ya da redeemed_subscription_id boşsa alarm yok; ikinci durumda satır sessizce continue edilir ve ilk 100'de yer tutar.

@ABB65
ABB65 merged commit 321d4a2 into main Oct 3, 2026
2 checks passed
@ABB65
ABB65 deleted the feat/migrate-provision-s2 branch October 3, 2026 15:21
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