Skip to content

fix(billing): move auto-intro metadata from schedule create to update#1389

Merged
jeanduplessis merged 4 commits intomainfrom
kiloclaw-billing-fix
Mar 23, 2026
Merged

fix(billing): move auto-intro metadata from schedule create to update#1389
jeanduplessis merged 4 commits intomainfrom
kiloclaw-billing-fix

Conversation

@jeanduplessis
Copy link
Contributor

@jeanduplessis jeanduplessis commented Mar 23, 2026

Summary

Problem

createAutoIntroSchedule passes metadata: { origin: 'auto-intro' } alongside from_subscription in stripe.subscriptionSchedules.create(). Stripe rejects this combination with HTTP 400, causing 100% failure on auto-intro schedule creation since commit df6ff4c (PR #1362). Affected intro-priced subscriptions never get the scheduled transition to the regular standard price.

Solution

  • Removed metadata from the subscriptionSchedules.create() call where Stripe forbids it alongside from_subscription.
  • Added metadata: { origin: 'auto-intro' } to the subsequent subscriptionSchedules.update() call, which already sets phases and end behavior.
  • Wrapped the update call in error recovery: if it fails, the half-created schedule is released so retry paths (Sweep 5 cron) can start fresh instead of encountering an untagged schedule they cannot identify.
  • Added race recovery: ensureAutoIntroSchedule and handleAutoIntroCreateRace now detect and claim untagged orphan schedules (create succeeded, update never ran) by tagging them as auto-intro and running repair.
  • Added a phases.length === 1 guard to the untagged-schedule claiming logic to avoid incorrectly claiming user-initiated plan switches or kilo-pass changes, which also create from_subscription schedules without metadata.origin but always have 2+ phases after their update completes.

Why this approach

The metadata must live on the schedule for downstream code to distinguish auto-intro schedules from user-initiated plan switches. Moving it to the update call is the minimal fix — the only alternative would be two separate update calls (one for metadata, one for phases), which doubles API calls for no benefit since both fields can be set atomically in one update.

Related issues

Verification

  • pnpm typecheck — pass
  • pnpm test -- src/routers/kiloclaw-billing-router.test.ts — 59/59 pass
  • Six-agent code review (security, logic, types, data, resources, style) — all clean after fixing the one WARNING
  • Pre-push hooks (format, lint, typecheck) — pass

Visual Changes

N/A

Reviewer Notes

  • Risk area: The window between create and update where the schedule exists without metadata. Three layers of mitigation: (1) release on update failure, (2) race recovery paths claim untagged orphans, (3) phases.length === 1 guard prevents incorrectly claiming user-initiated schedules.
  • Self-healing: After deploy, the next billing lifecycle cron run (Sweep 5) will automatically repair the 3 stranded subscriptions by calling ensureAutoIntroSchedule, which invokes the now-fixed createAutoIntroSchedule.
  • Rollback: Simple revert; no migrations or config changes.

Stripe rejects metadata on subscriptionSchedules.create when
from_subscription is set, causing 100% failure on auto-intro schedule
creation since commit df6ff4c. Move the origin metadata to the
subsequent update call and add error recovery to release half-created
schedules if the update fails.
@kilo-code-bot
Copy link
Contributor

kilo-code-bot bot commented Mar 23, 2026

Code Review Summary

Status: 1 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
src/lib/kiloclaw/stripe-handlers.ts 179 Claiming any untagged single-phase schedule as auto-intro still matches fresh from_subscription schedules from switchPlan() and Kilo Pass during their own create→update window, so auto-intro recovery can retag and rewrite a user-authored schedule before local state marks it as user-owned.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

None.

Files Reviewed (3 files)
  • src/lib/kiloclaw/stripe-handlers.ts - 1 issue
  • src/routers/kilo-pass-router.ts - 0 issues
  • src/routers/kiloclaw-router.ts - 0 issues

Fix these issues in Kilo Cloud


Reviewed by gpt-5.4-20260305 · 648,289 tokens

Stripe rejects metadata on subscriptionSchedules.create with
from_subscription, so a window exists where a schedule has no
origin tag. Make ensureAutoIntroSchedule and handleAutoIntroCreateRace
claim untagged schedules as auto-intro and repair them, closing the
race window where concurrent readers would skip orphaned schedules.
…edules

User-initiated plan switches and kilo-pass changes also create
from_subscription schedules without metadata.origin. Without a
phase-count check, ensureAutoIntroSchedule could incorrectly claim
and repair these as auto-intro schedules. Require phases.length === 1
(the from_subscription default) since createAutoIntroSchedule sets
metadata and phases atomically in the same update call.
…elper

Add metadata.origin to user-switch and kilo-pass-switch schedule
creation paths so they are never mistaken for auto-intro orphans.
Extract the duplicated orphan-detect-and-claim logic into a shared
claimIfAutoIntro() helper to prevent the heuristic from drifting
between ensureAutoIntroSchedule and handleAutoIntroCreateRace.
@jeanduplessis jeanduplessis merged commit 6572f77 into main Mar 23, 2026
18 checks passed
@jeanduplessis jeanduplessis deleted the kiloclaw-billing-fix branch March 23, 2026 12:25
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.

2 participants