Staging: validate reviewed legacy renewal catalog migration (#1155 + #1203) - #1220
Merged
Merged
Conversation
Permanent legacy legal and model-casing redirects now preserve query strings and canonicalize reverse-proxied www hosts. Website-only change; router deployment is not required.
Merge validated website SEO fallback noindex changes.
…e-promo-config # Conflicts: # website/src/app/sitemap.test.ts
* feat: configure homepage welcome promo * fix: refresh welcome promo settings immediately * fix: store featured model images in object storage * fix(website): align published blog SEO sources
* feat(report): add user-side usage report board (UTC+0 daily aggregate) New independent reporting module next to the existing ops daily report (controller/ops_report.go stays untouched): - usage_report_daily / usage_report_daily_model tables (AutoMigrate). - Lazy, idempotent per-UTC-day aggregation from users/tokens/top_ups/logs; today refreshed at most every 5min; no hot-path change. - GET /api/data/usage_report (admin): daily funnel + per-model usage, optional CSV export (daily funnel or date x model) for offline costing. - docs/usage-report.md documents metrics, SQL semantics, rollout and the planned Redis minute-counter phase (not wired yet). * feat(web): add Usage Report admin page (user-side funnel + model usage) New '用量报表' admin view, separate from the existing ops daily report: - route /usage-report (admin-guarded) + sidebar item + i18n labels - funnel first screen: daily KPI cards, daily detail table with totals, dual-axis bars (registered/activated key/first paid) + paid-amount line, 7d-rolling conversion trend, per-model stacked tokens area - CSV export for the daily funnel and the date x model usage slices - feeds from GET /api/data/usage_report (backend commit 2bad669) * fix(report): people-counted cohort conversion rates, capped <=100% Why the old number could reach 400%: '近7日滚动 Σ激活/Σ注册' divides two independent rolling windows while first-key events lag registrations; after the campaign, daily registrations collapsed while backlog users kept creating first keys, so Σactivated(7d) > Σregistered(7d). Fix (cohort, people-counted): - usage_report_daily gains activated_c7 (注册队列 7 日内建 Key 人数) and paid_c14 (建 Key 队列 14 日内首付人数); both are subsets of their denominators (registered / activated_key), so cohort rates never exceed 100%. Activation stays deduped per user (1 人多 Key = 1 人). - One-time recompute of stored rows via SchemaV bump. - Frontend drops the rolling formula; charts reg->key(7d cohort) and key->pay(14d cohort), hiding the most recent 7/14 days (cohort pending) and noting units are people. * feat(web): rebuild Usage Report page to approved v3 layout Single long scrolling page with anchor sub-nav, funnel first: - ① funnel: people KPI row, daily detail table, dual-axis bars (registered/activated key/first paid) + paid-amount line, cohort conversion trend, range funnel summary (auxiliary) - ② usage overview: calls/tokens + funnel KPIs, daily calls&tokens combo, today model share donut, range top models - ③ model usage: stacked area with calls/tokens switch, range model summary table, top models daily trend, CSV export buttons * fix(web): conversion trend chart -> bars (cohort rates, %) * fix(report): funnel daily table uses registration-cohort people counts The funnel table previously showed activation/first-paid by event day, so backlogged first-key creations (users registered earlier, verified later) made Activated(Key) > Registered for a row. All funnel people columns now use the SAME day's registration cohort (人) and are nested subsets: - Registered = users registered that day (enabled+verified) - Activated (7d cohort) = activated_c7 (⊆ Registered) - First Paid (14d cohort)= paid_reg_c14 (⊆ Registered, new field) Money/calls/tokens stay calendar-day figures. Pending recent cohorts show ⏳. SchemaV bumped to 2 so stored rows are recomputed once. * feat(report): same-day funnel caliber for fast C-side loop 漏斗以当天为准(快进快出 C 端): - usage_report_daily 新增 activated_day / paid_day:该日注册的人中 当天首次建 Key / 当天首次付费的人数(⊆ Registered,人) - 主表/明细表/KPI/双轴柱图/当天转化率柱图/区间汇总全部切到当天口径 - 转化率= 当天激活÷注册、当天首付÷注册,恒 ≤100%;今日为进行中数据 - SchemaV=3 自动重算存量行;7/14 日窗口字段保留仅作辅助 - docs 口径同步当天为准 * fix(report): exclude soft-deleted users from funnel raw SQL Registered is counted through GORM (auto deleted_at IS NULL scope), but the native-SQL activation/payment subqueries missed the soft-delete filter, so soft-deleted (banned farm) accounts still counted as activated/paid and could make Activated > Registered for a day. Add 'u.deleted_at IS NULL' to every user subquery (same-day activated/paid, cohort c7/reg-pay). SchemaV=4 forces recompute of stored rows. * fix(report): address code-review findings (real issues) Backend: - CSV written with c.String() treated content as a format string; use c.Data(..., buf.Bytes()) (content with '%' would corrupt the file). - controller fetched both daily and model rows before branching to CSV; CSV now fetches only the requested dimension. - EnsureUsageReportRange now computes the trailing window in UTC (non-UTC server clocks no longer shift the day range). - Per-date locks instead of one global mutex across all report reads. - Backfill NULL legacy rows (AutoMigrate adds nullable columns without defaults -> Go int scan would fail) before each row read; numeric columns now carry not null;default:0. - First-paid semantics fixed everywhere: compare MIN(first successful top-up per user) to first-key/registration instead of EXISTS any top-up, so repeat payments can no longer inflate first-pay cohorts. Frontend: - registered==0 conversion days return null (no fake 0% bars). - Model names no longer used directly as Recharts dataKeys; mapped to safe keys (s0.. / t0..), display names kept separate. - Model stacking pre-aggregated in one pass per date. * fix(report): drop unused event/long-window aggregates, no write path per read Follow-ups to review: - Remove event/long-window aggregates (activated_key, first_paid, c7/c14, reg-pay) from the daily compute: the UI only shows same-day metrics, and those MIN(...) GROUP BY queries forced full-history scans of tokens/top_ups on every date recompute. - Same-day paid uses EXISTS(settled same day) + NOT EXISTS(paid before registration) so it stays first-payment semantics without grouping the whole top_ups table per date. - NULL backfill runs once per process (only touches rows containing NULL) instead of an UPDATE on every Ensure() call.
Constraint: Retired plans must remain unavailable for quotes and purchases. Rejected: Re-add retired plans to the public catalog | would reopen legacy products for sale. Confidence: high Scope-risk: narrow Directive: Current-plan display must fall back only to the matching entitlement snapshot. Tested: 125 focused tests, frontend production build, local preview HTTP 200. Not-tested: Full main typecheck is blocked by pre-existing ops-report type errors.
Production website careers flow validated in a clean worktree from origin/main. Router deployment is not required; merging triggers the website production workflow for newapi-web.
Constraint: Retired plans must be unavailable to new buyers without changing existing wallet-renewal prices or benefits. Rejected: Reading mutable retired plan fields | could silently alter legacy billing and limits. Confidence: high Scope-risk: moderate Directive: Keep disabled-plan renewal gated by the current entitlement and its exact successful source-order snapshot. Tested: Focused wallet-renewal regression suite and go vet ./service. Not-tested: Full repository suite; an unrelated cache invalidation test is already failing on main.
Constraint: Wallet renewal must reproduce the successful source order even when a snapshot contains sub-minor-unit precision. Rejected: Charging the raw snapshot price | minor-unit comparison alone can hide a larger wallet quota debit. Confidence: high Scope-risk: narrow Directive: Treat the successful order UnitPrice as billing authority after validating its snapshot and currency. Tested: Focused retired-plan wallet renewal regression suite. Not-tested: Full repository suite.
Production release approved by user. PR quality job could not start because of repository Actions billing limits; local tests, typecheck, lint, and production build passed.
Constraint: Remove Limited time and 80% off from Go, Pro, and Max while preserving struck-through reference prices and backend-selected payable totals. Rejected: Removing reference prices | the billing page must retain the crossed-out comparison values. Confidence: high Scope-risk: narrow Directive: Keep subscription-card promotion labels separate from pricing and entitlement behavior. Tested: bun test src/features/wallet/components/subscription-plans-card.test.tsx (84 passed); bun run build; Prettier check; git diff --check; independent review. Not-tested: Full typecheck and ESLint remain blocked by unrelated origin/main baseline errors in ops-report/index.tsx and the existing subscription card effect.
…badges Remove subscription promo labels from billing cards
…1118) * feat(report): async historical fill so first page load never blocks Production first open of /api/data/usage_report ran a synchronous 30-day backfill and could take a long time on large log tables. - GET JSON: kicks off the trailing-window fill in the background (single in-process runner), serves already-persisted rows immediately and returns filling=true until the window is complete. - Front-end polls every 4s (max ~2min) and shows a '回填中' banner; rows appear progressively as each day lands. - CSV export stays synchronous (rare, admin-only, needs the full window). - Schema version untouched: new prod tables fill lazily on first open. * fix(report): review follow-ups on async fill - Remove the leftover synchronous EnsureUsageReportRange on the JSON path; only CSV fills synchronously (the interactive view was still blocked). - filling flag now reflects the real background-runner state (service.UsageReportFillRunning) instead of inferring from row count, so pages can't get stuck 'filling' forever on partial/failed backfills. - Front-end polling has no hard 2-minute cap: it keeps refetching every 4s until the server reports filling=false. - NULL column backfill no longer caches the first failure via sync.Once (retries until success), and runs at the start of the single-date ensure path too so single-date reads self-heal. * fix(report): bind filling flag to data completeness, not just runner state Cover the race where the background fill finishes between the row query and the response: filling is now (rows < days) || runner-running, and the front-end keeps polling while either holds, so it cannot stop early on a half-filled window. * fix(web): bound stale retries, keep unlimited polling while filling Review follow-up: infinite 4s polling when the backfill stalls or fails. - While the server reports filling=true (runner active) poll every 4s with no cap. - When the window is still incomplete but no fill is running (failure/stall), auto-retry at most 6 times, then stop and show a '回填未完成' banner with a manual Retry button (resets the counter and refetches). - The '重试' hint text and button live above the report; completed states stop polling.
…ation Constraint: Existing and newly created entitlements must retain contract-scoped counters until every reader understands version 1. Rejected: Activate entitlement-scoped keys with the schema migration | mixed-version routers would split usage and make rollback unsafe. Confidence: high Scope-risk: moderate Directive: Deploy this commit to console and router before creating any version-1 entitlement. Tested: targeted model/service window tests; go vet ./model ./service; git diff --check; independent code review Not-tested: real MySQL/PostgreSQL migration integration
…compat fix(subscription): prepare safe entitlement-scoped window rollout
Activate entitlement-scoped counters after the compatibility release so a new purchase does not inherit exhausted usage from its previous contract window. Constraint: Preserve existing subscribers' counters and enforce both the 5-hour and 7-day limits. Rejected: Delete contract Redis keys on purchase | Redis and DB are not atomic and old async settlements can repopulate the new window. Confidence: high Scope-risk: moderate Directive: Deploy only after 28b9e09 serves 100% on console and router; never roll back below it once v1 exists. Tested: Targeted Go window/purchase/controller tests; go vet; 85 wallet tests; targeted ESLint and Prettier; frontend build; diff inspection; independent review. Not-tested: Live MySQL/PostgreSQL default flip; full suite has unrelated baseline failures; GitNexus worktree index unavailable.
Keep the schema default at zero and opt in only at real grant creation. Preserve rollout and rollback grants on their original counters; remove in-place promotion from the runbook. Constraint: Existing usage and in-flight settlements must retain their original Redis identity. Rejected: Promote consumed rollout grants in place | It can reset usage and split admission from settlement. Confidence: high Scope-risk: moderate Directive: Deploy compatible readers first; do not merge or deploy the draft activation without release approval. Tested: Window migration, explicit grant creation, repurchase, late settlement, and targeted go vet. Not-tested: Live MySQL/PostgreSQL migration and multi-node production rollout.
Treat Stripe-confirmed paid invoices and their actual product identity as fulfillment authority, while retaining ownership and discount-record integrity. Preserve historical quotes and separate post-downgrade grant limits from pricing snapshots. Constraint: Adaptive Pricing, discounts, and taxes can change the paid currency and total without changing the purchased Price ID. Rejected: Compare provider totals to local quotes | Legitimate paid subscriptions remain pending. Confidence: high Scope-risk: moderate Directive: Preserve paid-status, item, quantity, customer, revision and idempotency checks; old acknowledged failures require an authorized replay after deployment. Tested: Targeted quota, Adaptive Pricing, invoice pagination, partial payment, product identity, discount integrity, replay, and downgrade renewal regressions; go vet and go build ./.... Not-tested: Real-money Stripe replay and live multi-database/multi-node rollout; broader suites retain documented baseline failures.
Merged SEO indexing cleanup. [skip ci]
Merged after local website typecheck, lint, sitemap tests, and webpack production build validation.
Constraint: Remove urgency and discount badges from subscription plan cards while preserving reference prices, payable totals, and recharge recall copy. Rejected: Removing shared translations or recharge promotion UI | those remain used outside subscription plan cards. Confidence: high Scope-risk: narrow Directive: Keep subscription plan badge absence assertions when merging future wallet and SEO changes. Tested: PR Check; 50 targeted wallet plan tests; production frontend build; Prettier; git diff --check. Not-tested: Existing unrelated typecheck, ESLint, and stale dialog-copy baseline failures remain.
Add an immutable, root-only catalog migration path that schedules new same-tier benefits only at a successful renewal boundary while retaining legacy orders and entitlements. Keep the first rollout sandbox-only and default staging deployment disabled. Constraint: Existing users, orders, used quota, and legacy 5h/7d limits must remain unchanged until paid renewal; production must not be enabled. Rejected: In-place plan edits or a permanent successor rule | they would rewrite legacy meaning or broaden the cohort beyond the explicit allowlist. Confidence: high Scope-risk: moderate Directive: Keep SUBSCRIPTION_CATALOG_MIGRATION_ENABLED false and the staging allowlist empty unless an isolated staging test is actively running. Tested: Focused model/controller/router/service migration suites, wallet concurrency x10, lock-order tests x10, go vet changed packages, git diff --check. Not-tested: Full service package remains affected by unrelated asset-binding/environment failures; race build lacks gcc.
…0260909 Honor purchased Stripe prices without redundant local amount checks
Persist provider schedule ownership proofs as fixed-width SHA-256 values so staging MySQL can schedule an eligible contract without truncation or a false zero-row update.\n\nConstraint: preserve the existing prefixed key format for IDs and idempotency tokens\nRejected: widen the production column | would require a broader schema rollout for a fixed-width digest\nConfidence: high\nScope-risk: narrow\nDirective: keep provider schedule fingerprints exactly 64 hexadecimal characters\nTested: go test ./service -run 'CatalogMigration' -count=1 -timeout=600s; git diff --check\nNot-tested: full repository suite and race detector on this host
* fix(website): exclude noindex blog locales from sitemap * fix(website): refresh published Blogger content * fix(website): validate Blogger detail pages before SEO output
* fix(website): exclude noindex blog locales from sitemap * fix(website): refresh published Blogger content * fix(website): validate Blogger detail pages before SEO output * perf(website): cache sitemap generation
Create subscription schedules from existing subscriptions without forbidden metadata, then configure ownership and renewal phases in a separate idempotent update. Recognize Stripe's initial current phase and fail closed on ambiguous schedule recovery.\n\nConstraint: Stripe rejects metadata when from_subscription is supplied, and legacy entitlements must remain untouched until the scheduled renewal.\nRejected: Reusing an unmarked attached schedule by phase shape alone | it could take ownership of an unrelated Stripe schedule.\nConfidence: high\nScope-risk: narrow\nDirective: Keep staging migration allowlisted and disabled by default; do not broaden provider recovery without an ownership proof.\nTested: go test ./service -run 'StripeCatalogMigration|CatalogMigrationUserAction|CancelCurrentSubscriptionRenewalPreemptsCatalogMigration' -count=1 -timeout=600s; go test ./service -run 'CatalogMigration|SubscriptionDiscount' -count=1 -timeout=600s; go vet ./service; git diff --check\nNot-tested: Full end-to-end renewal at the natural Stripe billing boundary
Keep the migration branch mergeable while preserving both purchased-price authority and pending-plan target validation.\n\nConstraint: The open main PR must include current main without weakening renewal ownership checks or changing production deployment behavior.\nRejected: Drop the upstream payment-validation changes | that would reintroduce price-authority regressions.\nConfidence: high\nScope-risk: moderate\nDirective: Review renewal price authority and catalog migration together whenever payment validation changes.\nTested: go test ./service -run 'CatalogMigration|RenewalPriceAuthority|PaidRenewal|SubscriptionDiscount' -count=1 -timeout=600s; git diff --cached --check\nNot-tested: Full repository suite after merge
Keep legacy entitlements untouched until a paid renewal and persist immutable deployment-mode facts for replay safety.\n\nConstraint: Production Stripe renewals must be opt-in and mode-isolated.\nRejected: Enabling live migration by default | would risk unintended cohort changes.\nConfidence: high\nScope-risk: moderate\nDirective: Enable production flags only after reviewing the immutable preview cohort.\nTested: Focused service/model migration suites; go vet ./model ./controller ./router ./service; git diff --check\nNot-tested: Full service suite retains unrelated environment failures.
…pe test mode The snapshot validator rejected every live-mode Stripe Price while the production sandbox gate requires live-mode facts, so production preview and scheduling could never succeed. Mode consistency stays enforced by ValidateCatalogMigrationStripeSandbox. Also checks in the review-fix implementation plan.
… paid invoices after discount release Renewal validation now proves ownership (subscription, item, customer, binding, contract), price id, currency and quantity only. Amount equality against local prices was removed everywhere: tax, customer credit balance, coupons and rounding legitimately change what Stripe collects and turned genuinely paid invoices into permanent, non-retryable failures. - A discount reservation that was already released no longer blocks the paid renewal grant; only the ledger commit is skipped. - Version 1 discount snapshots (reserved before typed renewal ownership existed) stay honored on bindings that gained a typed plan snapshot. - Re-preparing an already adjusted draft invoice no longer fails on the recomputed subtotal. - Discount fingerprints fall back to plan identity for renewals without a frozen snapshot (reached scheduled downgrades).
…nges A typed plan snapshot frozen for one plan can be left on a binding that later moves to another plan through an upgrade or downgrade. Treat a snapshot whose plan id no longer matches the binding as absent instead of failing every future renewal permanently, and clear it when a reached scheduled downgrade moves the binding to another catalog plan.
…ary and keep the wallet sweeper alive Renewal-boundary semantics are now explicit: the feature flag and contract allowlist gate admin preview/apply/cancel only. A scheduled cutover is a committed fact and still applies after the flag is closed. Only runtime drift (batch prepared on another service, environment or Stripe mode) blocks it, and that error is retryable rather than a permanent webhook ack. - Wallet loader treats every non-scheduled catalog intent as "no cutover" instead of erroring forever once an intent is applied. - Stripe loader applies compensation_required/needs_attention intents only when the paid invoice already bills the target price; otherwise the invoice is an ordinary legacy renewal. - RunWalletSubscriptionRenewalOnce logs and skips a failing contract instead of aborting the batch (which also skipped term advance, expiry and reset). - A contract paused for insufficient balance may have its entitlement expired by the expiry task; that state is accepted so the top-up retry and the cutover can still happen. - Ordinary typed-snapshot renewals no longer require the current catalog row to match the frozen snapshot.
…per-contract apply errors - Cancel persists status=cancelled on the batch. Get/refreshBatchSummary keep it sticky, and Apply refuses a cancelled batch, so a replayed apply can no longer schedule a contract whose first prepare failed before an intent row existed. - The summary counts syncing intents as in-flight (batch stays applying) instead of as failures. - processCatalogMigrationPreview logs each per-contract apply/resume error with batch and contract ids instead of discarding it.
…erflow Go's AddDate normalizes Jan 31 + 1 month to Mar 3. The schedule wrote a longer-than-monthly target phase for subscribers anchored on the 29th to 31st, and the ownership re-check (same expression) then never matched what Stripe stored, parking the intent in needs_attention. Clamp to the last day of the following month, preserving the time of day.
…g and respect reservation expiry - supersedeCatalogMigrationForUserAction takes the client's ExpectedChangeVersion and enforces it inside the locked transaction, before the Stripe schedule release and the intent supersede. A stale cancel now fails the precondition with no side effect instead of releasing the schedule and then overwriting the precondition to pass. - Wallet contracts (no provider binding) supersede a scheduled local cutover on user cancel/plan change, so the next renewal tick cannot silently undo the user's action. - The user-action attention marker is status-guarded: an intent that a concurrent paid renewal applied (or a cancel superseded) is never relabelled needs_attention and its contract is not frozen. - Busy checks (preview and user action) use the shared active-reservation predicate: consumed tombstones and expired leases no longer read as provider_lifecycle_busy. Database time is read on the transaction handle to avoid a second connection inside a locked transaction. - containsString is replaced by common.StringsContains.
… rules - Drop gorm type:longtext from the four snapshot columns; the dialector maps an unsized string to longtext on MySQL and text on PostgreSQL, so AutoMigrate no longer emits a MySQL-only type on PostgreSQL (Rule 2). - Route all JSON encoding through common.* instead of encoding/json in business code (Rule 1); the digest inputs are byte-identical. - Replace parseCatalogMigrationBool with common.GetEnvOrDefaultBool and delete the unused isCatalogMigrationTerminalProviderStatus. - Carry the applied cutover's batch id out of the paid-invoice transaction on the result instead of an unindexed intent lookup after every paid invoice.
The env export loop drops empty values and --update-env-vars is merge-only, so an allowlist set by hand on the running service could never be cleared by a deploy. Write the (possibly empty) allowlist explicitly in both production jobs.
…alidation Brings feat/staging-legacy-renewal-catalog-migration at e19548a (PR #1155 head after the #1203 review fixes) onto staging for a second sandbox validation of the legacy -> new catalog cutover at paid renewal. Resolution policy: - subscription/catalog-migration Go files taken wholesale from the feature head (they carry #1141 payment rules plus the 9 review-fix commits). - staging-only work (phone binding, usage report, SMS gate) kept as on staging; catalog routes/AutoMigrate registrations re-checked present. - gcp-deploy-staging.yml keeps staging's runtime hardening (--command/--args cleared) and adds SUBSCRIPTION_CATALOG_MIGRATION_PRODUCTION_ENABLED=false; the migration stays disabled with an empty allowlist on deploy. - web/ and website/ kept at staging state (no frontend changes in #1155).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Staging-only validation (second round, post-review)
Brings the #1155 feature head e19548a (= #1155 + the 9 review-fix commits from #1203) onto staging for a second Stripe sandbox E2E of the legacy → new catalog cutover at paid renewal.
The first round (PRs #1156/#1159/#1164, 2026-09-09..13) validated the pre-review code. This round re-validates after the review fixes: no Stripe amount comparison, resilient wallet sweeper, scheduled cutovers honoured at the boundary regardless of flag state, sticky batch cancellation, month-end phase arithmetic, version-checked cancel.
Merge policy
staging; catalog routes and AutoMigrate registrations re-checked.gcp-deploy-staging.ymlkeeps staging's--command="" --args=""runtime hardening and addsSUBSCRIPTION_CATALOG_MIGRATION_PRODUCTION_ENABLED=false.web/andwebsite/untouched (no frontend in Renew legacy subscriptions into the new catalog at paid renewal #1155).Deploy-time state
FLATKEY_DEPLOYMENT_ENV=stagingSUBSCRIPTION_CATALOG_MIGRATION_ENABLED=falseSUBSCRIPTION_CATALOG_MIGRATION_PRODUCTION_ENABLED=falseSUBSCRIPTION_CATALOG_MIGRATION_CONTRACT_ALLOWLIST=The feature is enabled only for explicitly selected staging contract IDs during the test, then disabled and cleared again.
Local verification on the merged tree
go build ./...,go vet ./model ./controller ./router ./serviceclean.go test ./service/(catalog migration, plan snapshot, invoice, discount, wallet renewal, price authority, upgrade, lifecycle) and./router/pass../controller/:TestStripeWebhookSubscriptionRecallAttributionAfterFulfillmentAndReplayRepairfails identically on the untouched feature head (pre-existing, already listed in Renew legacy subscriptions into the new catalog at paid renewal #1155).