Repository navigation
feat(billing): expire new paid credits one year after purchase - #7609
Conversation
Add a disabled-by-default paid-credit expiry policy. When PAID_CREDIT_EXPIRATION_EFFECTIVE_AT is set, new paid credits expire one calendar year after purchase, and each Kilo Pass allotment expires one year after issuance. Apple in-app purchases never expire. Existing balances and previously disclosed terms are unchanged. Expiration spends the earliest deadline first, keeps refunds, reversals and transfers lot-aware, excludes KiloClaw settlement pass-through, and runs from a due-owner cron. Balance reads and expiration no longer trigger auto-top-up. Purchase flows on web and mobile show the applicable terms, and receipts, history and emails show the stored deadline.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryThe incremental head only adds clean merge resolutions and a small import/fixture fix on top of the previous review, with the migration renumbered to Assumptions: Files Reviewed (11 files)
The remaining 600+ files in the incremental range are upstream Previous Review Summaries (12 snapshots, latest commit 27be16d)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 27be16d)Status: No Issues Found | Recommendation: Merge Executive SummaryThe incremental head deletes the redundant Assumptions: Files Reviewed (12 files)
Previous review (commit 1925914)Status: No Issues Found | Recommendation: Merge Executive SummaryThe incremental head only removes an unused Assumptions: Files Reviewed (3 files)
Previous review (commit 8f9b877)Status: 1 Issue Found | Recommendation: Address before merge Executive SummaryThe incremental work is a Assumptions: Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (11 areas)
Fix these issues in Kilo Cloud Previous review (commit 5db03a0)Status: 1 Issue Found | Recommendation: Address before merge Executive SummaryThe branch work since the previous review is a type-only follow-up ( Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (16 files)
Previous review (commit 0fae733)Status: No Issues Found | Recommendation: Merge Executive SummaryThe only PR-authored change since the previous review is a spec clarification that Google Play Kilo Pass base credits, like App Store ones, never expire; it matches the implemented Scope NoteReviewed commit Files Reviewed (1 file)
Previous review (commit a82811b)Status: 1 Issue Found | Recommendation: Address before merge Executive SummaryThe incremental follow-ups are sound, but one previously raised warning is still unresolved: Google Play Kilo Pass base credits are hardcoded to legacy terms, so post-activation Play purchases and renewals never receive the one-year expiry described in the spec. Scope NoteReviewed the six commits authored since the previous review ( Overview
Issue Details (click to expand)WARNING
Files Reviewed (13 files)
Also verified the merge conflict resolutions in Fix these issues in Kilo Cloud Previous review (commit d50974f)Status: No Issues Found | Recommendation: Merge Executive SummaryThe incremental update is test-only: it makes the legacy-expiration replay fixture coherent (refreshed acquired total, an extra non-expiring lot, an explicit Scope NoteReviewed the changes authored since the previous review ( Files Reviewed (2 files)
Previous review (commit a45b7bb)Status: No Issues Found | Recommendation: Merge Executive SummaryThe incremental update hardens paid-credit expiry accounting under load (jsonb-batched row updates, 1000-row insert chunking, normalized-lot-aware balance repair, and a tighter paid-principal predicate) and adds regression tests; no new issues were found. Scope NoteReviewed the changes this branch authored since the previous review ( Files Reviewed (13 files)
Previous review (commit b4732e3)Status: No Issues Found | Recommendation: Merge Executive SummaryThe only incremental change since the previous review corrects the transfer-in lot's Files Reviewed (2 files)
Previous review (commit ffecafd)Status: No Issues Found | Recommendation: Merge Executive SummaryThe only change since the previous review is a one-line reorder in Files Reviewed (1 file)
Previous review (commit 81a1a4d)Status: No Issues Found | Recommendation: Merge Executive SummaryThe eleven follow-up commits since the last review resolve all seven previously reported findings, and each fix is correct, internally consistent, and backed by a targeted test; no new issues were found in the changed lines. Verification of previous findings (all fixed)
Files Reviewed (18 changed files + generated)
Previous review (commit 4fe4a35)Status: 7 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
[Snapshot truncated.] Additional previous summary content was truncated to keep this comment within platform limits. Reviewed by deepseek-flash-latest · Input: 65.2K · Output: 11.4K · Cached: 842.6K Review guidance: REVIEW.md from base branch |
…action Grant success tests now use persisted users because grants reconcile the owner row.
…ry-impl # Conflicts: # .kilo/skills/kilo-mcp/SKILL.md # packages/db/src/migrations/meta/0280_snapshot.json # packages/db/src/migrations/meta/_journal.json
…ry-impl # Conflicts: # .kilo/skills/kilo-mcp/SKILL.md # apps/web/vercel.json # packages/db/src/migrations/meta/0283_snapshot.json # packages/db/src/migrations/meta/_journal.json # packages/web-shared/src/lib/ai-gateway/typesafe/handler.ts # packages/web-shared/src/lib/organizations/organization-usage.ts # packages/web-shared/src/lib/user/balance.ts
…ry-impl # Conflicts: # packages/db/src/migrations/meta/0287_snapshot.json # packages/db/src/migrations/meta/_journal.json # packages/web-shared/src/lib/ai-gateway/handlers/audio-transcriptions.ts # packages/web-shared/src/lib/ai-gateway/handlers/embeddings.ts # packages/web-shared/src/lib/ai-gateway/typesafe/handler.test.ts # packages/web-shared/src/lib/ai-gateway/typesafe/handler.ts
…ry-impl # Conflicts: # packages/db/src/migrations/meta/0303_snapshot.json # packages/db/src/migrations/meta/_journal.json




Summary
New paid credits can expire one calendar year after purchase. The policy is off by default: until
PAID_CREDIT_EXPIRATION_EFFECTIVE_ATis set, every new purchase keeps legacy (non-expiring) terms and the purchase flows show no new expiry claim.When activated:
Accounting
paid_credit_policy,credit_source,source_transaction_id,consumed_microdollars,retired_microdollars,refund_principal_microdollars, plus threeCONCURRENTLYindexes.min(used, acquired), so credit that repays prior debt cannot expire later.original_baseline_microdollars_usedkeeps the usage prefix./api/cron/expire-creditsdue-owner cron. User failures are isolated; organizations are processed in bounded batches.settlementon both legs and never becomes spendable or reported revenue.Surfaces
_1year_v2Play SKUs are used only by updated clients after activation, and legacy receipts still verify and recover. Profile and purchase screens show the next expiring amount.Not changed
charge.refundedhandler still does not claw back credits; that behavior is unchanged and out of scope.Before activation
_1year_v2Google Play credit products.PAID_CREDIT_EXPIRATION_EFFECTIVE_ATto an announced future instant.Verification
Run on an isolated VM with a fake account, local Postgres, and Stripe test mode:
@kilocode/db,@kilocode/web-shared,web,kilo-app, andkiloclaw-billingtypecheck; web, mobile, db and billing lint; mobile unused-code and i18n checks; formatting.Adversarial verification and hardening
Verified against isolated PostgreSQL with synthetic accounts:
No live store purchase or production mutation was performed during this pass.
Final follow-up: the child-balance regression now verifies committed usage, and the legacy replay fixture includes complete funding and an explicit expiry closure. All CI checks passed on the final head. Kilobot recommends merge; human approval remains required.
End-to-end stress pass
A second pass drove grants, spend, expiry, refunds, transfers, repair, reporting, and the web, email and Android surfaces against isolated stacks with synthetic accounts, Stripe test mode, and an independent balance model. It found these issues, now fixed:
credits_expiredclosures. A lot already closed by an Orb row failed the whole batch, so the cron returned 500 and stalled every due organization. The batch now uses the same closure categories as per-owner reconciliation. The cron also falls back to per-organization reconciliation when a batch fails.Known and accepted:
maindoes the same.Stack readiness verification
0303_fine_moonstone; its DDL is unchanged, including concurrent-index transaction boundaries. Migration application and schema consistency checks pass.Latest base sync preserves both the paid-policy imports and native device-session invalidation. Router and credit-block regressions pass after this import-only conflict resolution. The live policy and balance routes pass against the isolated database. No billing behavior changed in this sync.
Latest conflict refresh
0303_fine_moonstone; the complete SQL is byte-identical to the previous expiry migration.