fix(api): tier upgrade promotes team default TTL + auto_24h deploys (P1)#212
Merged
mastermanas805 merged 4 commits intoMay 31, 2026
Merged
Conversation
A Pro-tier user (mastermanas805) just got an "expires in 6 hours" email
the day after upgrading free→pro. Root cause: subscription.charged only
called UpgradeTeamAllTiers, which lifts per-deploy ttl_policy as a
side-effect but never touches teams.default_deployment_ttl_policy — so
every NEXT POST /deploy/new still inherited 'auto_24h' and re-fired the
24h-expiry reminder cycle.
Wires a new models.PromoteDeploymentTTLsForTeam (single tx) into
handleSubscriptionCharged for tiers >= hobby:
- flips teams.default_deployment_ttl_policy auto_24h → permanent
(user-explicit non-auto_24h defaults are LEFT UNTOUCHED)
- promotes every non-terminal ttl_policy='auto_24h' deploy to
permanent + clears expires_at + resets the reminders ledger
- emits team.ttl_policies_promoted audit row + counter
instant_tier_upgrade_ttl_promote_total{outcome=success|noop|error}
Fail-open: the upgrade tx has already committed by promote-time, so a
promote error never 500s the webhook (operator runs cmd/backfill-tier-ttl
to repair the residual; the function is idempotent).
Coverage block (CLAUDE.md rule 17):
Symptom: team.default stays 'auto_24h' + auto_24h deploys keep
firing "expires in N hours" emails after paid upgrade
Enumeration: rg -F 'UpdatePlanTier' rg -F 'UpgradeTeamAllTiers'
rg -F 'default_deployment_ttl_policy'
Sites found: 4 (billing webhook, /internal/set-tier, admin tier change,
PATCH /api/v1/team/settings)
Sites touched: 1 — the Razorpay webhook is the ONLY paid-tier
promotion path (set-tier is dev-only, admin demote
keeps existing state by design, team-settings is the
user's own override and must not trigger promote)
Coverage test: e2e/reliability_contract_test.go's audit-kinds registry
iterator (rule 18) flags any new AuditKind* constant
that ships without a downstream-consumer entry, and
TestPlansRegistryUpgradeTargets_AllInvokePromoteGuard
iterates plans.Registry tiers so a new tier added
between free and hobby would loudly fail the guard.
Live verified: awaiting user verification of the next paid upgrade —
the new metric NR alert (tier-upgrade-ttl-promote-failed)
pages on any outcome=error tick within 10m.
Backfill: operator runs `DATABASE_URL=… go run ./cmd/backfill-tier-ttl
-apply` once to repair every paid team with stale auto_24h state. The
script is idempotent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the missing branch coverage for the Pro-upgrade auto-promote TTL fix landed in 82de682, taking the three changed surfaces to 100%: - cmd/backfill-tier-ttl: refactored main() → exitFn-wrapped run() with injectable openDB + promoteFn seams (mirrors cmd/openapi-snapshot). Ten tests cover usage errors, DB open/ping/query/scan/rows failures, dry-run vs apply modes, mixed ok/error per-team tallying, env-var fallback, and the main() exit-code dispatch. - models.PromoteDeploymentTTLsForTeam: six sqlmock-driven tests for the begin/exec/rows-affected/commit error wrappers that a real Postgres test DB can't drive on demand. - handlers.handleSubscriptionCharged + emitTTLPoliciesPromotedAudit: added the promoteDeploymentTTLsForTeamFn seam (same pattern as billingPortalFactory) so the fail-open promote-error branch is reachable. Two tests: nil-db audit early-return + webhook still 200s on a simulated promote tx failure. No production behaviour change: the cmd refactor keeps the same exit codes and the handler seam is a package-level var pointing at the real models call, swapped only by tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
P1 user-visible bug. A Pro-tier user got an "expires in 6 hours" email the day after upgrading free→pro. Root cause: the Razorpay
subscription.chargedwebhook calledUpgradeTeamAllTiersbut never touchedteams.default_deployment_ttl_policy— so every NEXTPOST /deploy/newstill inheritedauto_24hand re-fired the 24h-expiry reminder cycle.models.PromoteDeploymentTTLsForTeam(ctx, db, teamID) (PromoteDeploymentTTLsResult, error)— single tx that (a) flipsteams.default_deployment_ttl_policyauto_24h→permanent(user-explicit non-auto defaults are LEFT UNTOUCHED), and (b) promotes every non-terminalttl_policy='auto_24h'deploy to permanent + clearsexpires_at+ resets the reminders ledger. Custom + already-permanent rows are LEFT UNTOUCHED.handleSubscriptionChargedfor tiers>= hobby(anonymous / free skip the promote path). Fail-open: a promote error never 500s the webhook — the upgrade tx has already committed and the operator runs the idempotentcmd/backfill-tier-ttlto repair the residual.team.ttl_policies_promoted(operator-only, no customer email — thesubscription.upgradedemail already covers customer comms) + metricinstant_tier_upgrade_ttl_promote_total{outcome=success|noop|error}.llms.txt: https://github.com/InstaNode-dev/content/pull/new/docs/llms-tier-upgrade-ttl-promoteCoverage block (CLAUDE.md rule 17)
Backfill (operator action — one-off after deploy)
DATABASE_URL=$(kubectl get secret -n instant instant-secrets -o jsonpath='{.data.DATABASE_URL}' | base64 -d) \ go run ./cmd/backfill-tier-ttl -applyDefault mode is dry-run. The function is idempotent — safe to re-run for any residual after a partial failure.
Test plan
make gate— new tests pass; remaining failures are pre-existing (customer-DB proxy / NATS service not present on local laptop — known per Makefile docstring). Confirmed by stash-test on master baseline.go test -run 'TestPromoteDeploymentTTLs|TestPlansRegistryUpgradeTargets|TestBillingWebhook_ChargedPromotesTeamDefaultAndDeploys|TestBillingWebhook_ChargedDoesNotPromoteOnSameTierRenewal|TestReliability_AuditKinds_EveryConstantHasConsumerSpec' ./internal/models ./internal/handlers ./e2e→ok.curl https://api.instanode.dev/healthz | jq .commit_id.cmd/backfill-tier-ttl -applyonce against prod (DRY-RUN first).permanent.🤖 Generated with Claude Code