From a93dae5cbb5785cf30fb1ba6ce2c612711279ea6 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Mon, 22 Jun 2026 19:06:44 +0200 Subject: [PATCH 1/4] fix(kiloclaw): serialize credit billing mutations --- apps/web/src/lib/kiloclaw/credit-billing.ts | 123 ++++++++-------- .../kiloclaw/stripe-funded-settlement.test.ts | 132 ++++++++++++++++++ .../routers/kiloclaw-billing-router.test.ts | 81 +++++++++++ apps/web/src/routers/user-router.ts | 2 +- 4 files changed, 275 insertions(+), 63 deletions(-) diff --git a/apps/web/src/lib/kiloclaw/credit-billing.ts b/apps/web/src/lib/kiloclaw/credit-billing.ts index 215fab87c4..a9091c1900 100644 --- a/apps/web/src/lib/kiloclaw/credit-billing.ts +++ b/apps/web/src/lib/kiloclaw/credit-billing.ts @@ -484,7 +484,6 @@ export async function applyStripeFundedKiloClawPeriod( } = params; const amountCents = Math.round(amountMicrodollars / 10_000); - const periodStartDate = periodStart.slice(0, 10); // YYYY-MM-DD let wasSuspended = false; let resolvedInstanceId: string | undefined; @@ -497,9 +496,8 @@ export async function applyStripeFundedKiloClawPeriod( // shouldSendSubscriptionStartedEmailForActivation. let shouldSendSubscriptionStartedEmailForNewSettlement = false; let requiresProviderNonRenewal = false; - // Set when the primary settlement insert was a duplicate (processTopUp - // returned false). In that case the downstream email side effect may not - // have run yet and we attempt best-effort recovery after commit. + // Tracks only a duplicate deposit from processTopUp so post-commit email + // recovery can run. A deduction conflict aborts and rolls back settlement. let settlementWasDuplicate = false; await db.transaction(async tx => { @@ -661,7 +659,7 @@ export async function applyStripeFundedKiloClawPeriod( return; } - const deductionCategory = `kiloclaw-settlement:${stripeSubscriptionId}:${periodStartDate}`; + const deductionCategory = `kiloclaw-settlement:${stripeSubscriptionId}:payment:${stripePaymentId}`; const deductionResult = await tx .insert(credit_transactions) .values({ @@ -676,20 +674,19 @@ export async function applyStripeFundedKiloClawPeriod( }) .onConflictDoNothing(); - if ((deductionResult.rowCount ?? 0) > 0) { - await tx - .update(kilocode_users) - .set({ - total_microdollars_acquired: sql`${kilocode_users.total_microdollars_acquired} - ${amountMicrodollars}`, - }) - .where(eq(kilocode_users.id, userId)); - } else { - logInfo('Duplicate deduction skipped, proceeding with subscription update', { - user_id: userId, - deductionCategory, - }); + if ((deductionResult.rowCount ?? 0) === 0) { + throw new Error( + `Stripe-funded KiloClaw settlement deduction conflict for payment ${stripePaymentId}` + ); } + await tx + .update(kilocode_users) + .set({ + total_microdollars_acquired: sql`${kilocode_users.total_microdollars_acquired} - ${amountMicrodollars}`, + }) + .where(eq(kilocode_users.id, userId)); + const updateSet = { instance_id: targetRow.instance_id, stripe_subscription_id: stripeSubscriptionId, @@ -1238,22 +1235,7 @@ export async function enrollWithCredits(params: { throw new CreditEnrollmentError('commit_unavailable', message); } - // Step 1: Read current state - const [user] = await db - .select({ - total_microdollars_acquired: kilocode_users.total_microdollars_acquired, - microdollars_used: kilocode_users.microdollars_used, - kilo_pass_threshold: kilocode_users.kilo_pass_threshold, - }) - .from(kilocode_users) - .where(eq(kilocode_users.id, userId)) - .limit(1); - - if (!user) { - logError('Credit enrollment failed: user not found', { user_id: userId, instanceId }); - throw new CreditEnrollmentError('user_not_found', 'User not found'); - } - + // Step 1: Read current target state const [targetInstance] = await db .select({ id: kiloclaw_instances.id, @@ -1353,31 +1335,7 @@ export async function enrollWithCredits(params: { // Save suspension state for post-transaction auto-resume (spec rule 4) const wasSuspended = !!existingSub?.suspended_at; - // Step 2: Check effective balance (spec rule 3) - // Effective balance = raw balance + projected Kilo Pass bonus that would - // be awarded after the deduction by maybeIssueKiloPassBonusFromUsageThreshold. - // The deduction increments microdollars_used, so project the post-deduction - // value to correctly evaluate whether the spend crosses the bonus threshold. - const balance = user.total_microdollars_acquired - user.microdollars_used; - const { effectiveBalanceMicrodollars: effectiveBalance } = await getEffectiveCreditBalancePreview( - { - userId, - balanceMicrodollars: balance, - microdollarsUsed: user.microdollars_used, - kiloPassThreshold: user.kilo_pass_threshold, - costMicrodollars, - } - ); - - if (effectiveBalance < costMicrodollars) { - const shortfall = costMicrodollars - effectiveBalance; - throw new CreditEnrollmentError( - 'insufficient_credits', - `Insufficient credit balance. You need ${shortfall} more microdollars to enroll.` - ); - } - - // Step 3: Single DB transaction (spec rule 5) + // Step 2: Serialize the balance decision and enrollment mutation. const now = new Date(); const periodMonths = plan === 'commit' ? 6 : 1; const periodEnd = addMonths(now, periodMonths); @@ -1395,6 +1353,22 @@ export async function enrollWithCredits(params: { const trialEndEntityId = existingSub?.status === 'trialing' ? existingSub.id : undefined; await db.transaction(async tx => { + const [user] = await tx + .select({ + total_microdollars_acquired: kilocode_users.total_microdollars_acquired, + microdollars_used: kilocode_users.microdollars_used, + kilo_pass_threshold: kilocode_users.kilo_pass_threshold, + }) + .from(kilocode_users) + .where(eq(kilocode_users.id, userId)) + .for('update') + .limit(1); + + if (!user) { + logError('Credit enrollment failed: user not found', { user_id: userId, instanceId }); + throw new CreditEnrollmentError('user_not_found', 'User not found'); + } + const [transactionTargetInstance] = await tx .select({ destroyedAt: kiloclaw_instances.destroyed_at }) .from(kiloclaw_instances) @@ -1435,7 +1409,32 @@ export async function enrollWithCredits(params: { ); } - // 5a: Insert negative credit transaction with period-encoded idempotency key + const projectedUsage = user.microdollars_used + costMicrodollars; + const effectiveThreshold = getEffectiveKiloPassThreshold(user.kilo_pass_threshold); + const kiloPassSubscription = + effectiveThreshold !== null && projectedUsage >= effectiveThreshold + ? await getKiloPassStateForUser(tx, userId) + : null; + const balance = user.total_microdollars_acquired - user.microdollars_used; + const { effectiveBalanceMicrodollars: effectiveBalance } = + await getEffectiveCreditBalancePreview({ + userId, + balanceMicrodollars: balance, + microdollarsUsed: user.microdollars_used, + kiloPassThreshold: user.kilo_pass_threshold, + costMicrodollars, + subscription: kiloPassSubscription, + }); + + if (effectiveBalance < costMicrodollars) { + const shortfall = costMicrodollars - effectiveBalance; + throw new CreditEnrollmentError( + 'insufficient_credits', + `Insufficient credit balance. You need ${shortfall} more microdollars to enroll.` + ); + } + + // Insert negative credit transaction with period-encoded idempotency key. const deductionResult = await tx .insert(credit_transactions) .values({ @@ -1473,8 +1472,8 @@ export async function enrollWithCredits(params: { ); } - // 5b: Atomically increment microdollars_used so the deduction counts - // as spend toward the Kilo Pass bonus unlock threshold. + // Increment microdollars_used so the deduction counts as spend toward + // the Kilo Pass bonus unlock threshold. await tx .update(kilocode_users) .set({ @@ -1482,7 +1481,7 @@ export async function enrollWithCredits(params: { }) .where(eq(kilocode_users.id, userId)); - // 5c: Upsert subscription row as pure credit + // Upsert subscription row as pure credit. const commitEndsAt = plan === 'commit' ? periodEndIso : null; const [mutatedSubscription] = await tx .insert(kiloclaw_subscriptions) diff --git a/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts b/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts index a9bec6befb..725218c242 100644 --- a/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts +++ b/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts @@ -579,4 +579,136 @@ describe('Stripe-funded KiloClaw settlement', () => { -12_340_000, 12_340_000, ]); }); + + it('keeps distinct payments for the same period balance-neutral', async () => { + const user = await insertTestUser({ id: 'settlement-distinct-payment-user' }); + const instanceId = '45454545-4545-4454-8454-454545454545'; + const subscriptionId = '67676767-6767-4676-8676-676767676767'; + const stripeSubscriptionId = 'sub_distinct_payment_same_period'; + const periodStart = '2026-06-01T00:00:00.000Z'; + const periodEnd = '2026-07-01T00:00:00.000Z'; + + await insertPersonalInstance({ id: instanceId, userId: user.id }); + await db.insert(kiloclaw_subscriptions).values({ + id: subscriptionId, + user_id: user.id, + instance_id: instanceId, + stripe_subscription_id: stripeSubscriptionId, + payment_source: 'credits', + kiloclaw_price_version: CURRENT_KILOCLAW_PRICE_VERSION, + plan: 'standard', + status: 'active', + current_period_start: periodStart, + current_period_end: periodEnd, + credit_renewal_at: periodEnd, + }); + + const settlement = { + userId: user.id, + metadataInstanceId: instanceId, + stripeSubscriptionId, + plan: 'standard', + priceVersion: CURRENT_KILOCLAW_PRICE_VERSION, + amountMicrodollars: 55_000_000, + periodStart, + periodEnd, + } satisfies Omit[0], 'stripePaymentId'>; + + await expect( + applyStripeFundedKiloClawPeriod({ + ...settlement, + stripePaymentId: 'ch_distinct_payment_first', + }) + ).resolves.toBe(true); + await expect( + applyStripeFundedKiloClawPeriod({ + ...settlement, + stripePaymentId: 'ch_distinct_payment_second', + }) + ).resolves.toBe(true); + + await expect(readUser(user.id)).resolves.toMatchObject({ + total_microdollars_acquired: 0, + }); + + const transactions = await db + .select({ + amountMicrodollars: credit_transactions.amount_microdollars, + creditCategory: credit_transactions.credit_category, + }) + .from(credit_transactions) + .where(eq(credit_transactions.kilo_user_id, user.id)); + expect(transactions.map(row => row.amountMicrodollars).sort((a, b) => a - b)).toEqual([ + -55_000_000, -55_000_000, 55_000_000, 55_000_000, + ]); + expect( + new Set( + transactions + .filter(transaction => transaction.amountMicrodollars < 0) + .map(transaction => transaction.creditCategory) + ).size + ).toBe(2); + }); + + it('rolls back the deposit and subscription mutation when its deduction cannot be recorded', async () => { + const user = await insertTestUser({ id: 'settlement-deduction-conflict-user' }); + const instanceId = '89898989-8989-4898-8989-898989898989'; + const subscriptionId = '90909090-9090-4090-8090-909090909090'; + const stripeSubscriptionId = 'sub_deduction_conflict'; + const stripePaymentId = 'ch_deduction_conflict'; + + await insertPersonalInstance({ id: instanceId, userId: user.id }); + await db.insert(kiloclaw_subscriptions).values({ + id: subscriptionId, + user_id: user.id, + instance_id: instanceId, + stripe_subscription_id: stripeSubscriptionId, + payment_source: 'credits', + kiloclaw_price_version: CURRENT_KILOCLAW_PRICE_VERSION, + plan: 'standard', + status: 'past_due', + current_period_start: '2026-05-01T00:00:00.000Z', + current_period_end: '2026-06-01T00:00:00.000Z', + credit_renewal_at: '2026-06-01T00:00:00.000Z', + past_due_since: '2026-06-01T00:00:00.000Z', + }); + await db.insert(credit_transactions).values({ + kilo_user_id: user.id, + amount_microdollars: -55_000_000, + is_free: false, + credit_category: `kiloclaw-settlement:${stripeSubscriptionId}:payment:${stripePaymentId}`, + check_category_uniqueness: true, + }); + + await expect( + applyStripeFundedKiloClawPeriod({ + userId: user.id, + metadataInstanceId: instanceId, + stripeSubscriptionId, + stripePaymentId, + plan: 'standard', + priceVersion: CURRENT_KILOCLAW_PRICE_VERSION, + amountMicrodollars: 55_000_000, + periodStart: '2026-06-01T00:00:00.000Z', + periodEnd: '2026-07-01T00:00:00.000Z', + }) + ).rejects.toThrow('settlement deduction conflict'); + + await expect(readUser(user.id)).resolves.toMatchObject({ + total_microdollars_acquired: 0, + }); + await expect(readSubscription(subscriptionId)).resolves.toMatchObject({ + status: 'past_due', + current_period_start: '2026-05-01 00:00:00+00', + current_period_end: '2026-06-01 00:00:00+00', + past_due_since: '2026-06-01 00:00:00+00', + }); + + const transactions = await db + .select() + .from(credit_transactions) + .where(eq(credit_transactions.kilo_user_id, user.id)); + expect(transactions).toHaveLength(1); + expect(transactions[0]?.stripe_payment_id).toBeNull(); + }); }); diff --git a/apps/web/src/routers/kiloclaw-billing-router.test.ts b/apps/web/src/routers/kiloclaw-billing-router.test.ts index b0a2b5d5c3..58ca2be272 100644 --- a/apps/web/src/routers/kiloclaw-billing-router.test.ts +++ b/apps/web/src/routers/kiloclaw-billing-router.test.ts @@ -7584,6 +7584,87 @@ describe('enrollWithCredits', () => { ); }); + it('serializes concurrent enrollment balance decisions for different instances', async () => { + await giveUserCredits(user.id, 55_000_000); + const [firstInstance, secondInstance] = await db + .insert(kiloclaw_instances) + .values([ + { + user_id: user.id, + sandbox_id: `ki_concurrent_first_${user.id}`, + }, + { + user_id: user.id, + sandbox_id: `ki_concurrent_second_${user.id}`, + }, + ]) + .returning(); + + await db.insert(kiloclaw_subscriptions).values([ + { + user_id: user.id, + instance_id: firstInstance.id, + plan: 'standard', + status: 'canceled', + payment_source: 'credits', + kiloclaw_price_version: CURRENT_KILOCLAW_PRICE_VERSION, + }, + { + user_id: user.id, + instance_id: secondInstance.id, + plan: 'standard', + status: 'canceled', + payment_source: 'credits', + kiloclaw_price_version: CURRENT_KILOCLAW_PRICE_VERSION, + }, + ]); + + const { enrollWithCredits } = await import('@/lib/kiloclaw/credit-billing'); + const results = await Promise.allSettled([ + enrollWithCredits({ + userId: user.id, + instanceId: firstInstance.id, + plan: 'standard', + hadPaidSubscription: true, + }), + enrollWithCredits({ + userId: user.id, + instanceId: secondInstance.id, + plan: 'standard', + hadPaidSubscription: true, + }), + ]); + + expect(results.filter(result => result.status === 'fulfilled')).toHaveLength(1); + expect(results.filter(result => result.status === 'rejected')).toHaveLength(1); + expect(results.find(result => result.status === 'rejected')).toEqual( + expect.objectContaining({ + reason: expect.objectContaining({ reason: 'insufficient_credits' }), + }) + ); + + const [updatedUser] = await db + .select({ microdollarsUsed: kilocode_users.microdollars_used }) + .from(kilocode_users) + .where(eq(kilocode_users.id, user.id)); + expect(updatedUser.microdollarsUsed).toBe(55_000_000); + + const deductions = await db + .select({ amountMicrodollars: credit_transactions.amount_microdollars }) + .from(credit_transactions) + .where(eq(credit_transactions.kilo_user_id, user.id)); + expect(deductions.filter(row => row.amountMicrodollars < 0)).toHaveLength(1); + + const subscriptions = await db + .select({ status: kiloclaw_subscriptions.status }) + .from(kiloclaw_subscriptions) + .where(eq(kiloclaw_subscriptions.user_id, user.id)); + expect(subscriptions.filter(subscription => subscription.status === 'active')).toHaveLength(1); + expect(subscriptions.filter(subscription => subscription.status === 'canceled')).toHaveLength( + 1 + ); + }); + it('deduction is idempotent via credit_category uniqueness', async () => { await createCreditEnrollmentAnchor(user.id); await giveUserCredits(user.id, 50_000_000); diff --git a/apps/web/src/routers/user-router.ts b/apps/web/src/routers/user-router.ts index 5e62e1e8d8..0b76255cf4 100644 --- a/apps/web/src/routers/user-router.ts +++ b/apps/web/src/routers/user-router.ts @@ -118,7 +118,7 @@ type RawDeduction = { * * Pure-credit categories: `kiloclaw-subscription:{instanceId}:YYYY-MM` * `kiloclaw-subscription-commit:{instanceId}:YYYY-MM` - * Settlement categories: `kiloclaw-settlement:{stripeSubId}:YYYY-MM-DD` + * Settlement categories: `kiloclaw-settlement:{stripeSubId}:payment:{stripePaymentId}` * * Returns the instance UUID for pure-credit categories, or null for * settlement categories (which embed the Stripe subscription ID instead). From 054144effd3d7c0f27a5113e5e7b0abd13a1fda7 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Mon, 22 Jun 2026 20:36:10 +0200 Subject: [PATCH 2/4] fix(kiloclaw): close billing concurrency gaps --- .../src/lib/kilo-pass/cancel-and-refund.ts | 20 +- apps/web/src/lib/kilo-pass/state.ts | 8 +- apps/web/src/lib/kiloclaw/credit-billing.ts | 259 ++++++++++++------ .../kiloclaw/stripe-funded-settlement.test.ts | 97 +++++++ .../routers/kiloclaw-billing-router.test.ts | 62 +++++ 5 files changed, 344 insertions(+), 102 deletions(-) diff --git a/apps/web/src/lib/kilo-pass/cancel-and-refund.ts b/apps/web/src/lib/kilo-pass/cancel-and-refund.ts index 2141359fde..3ffb8c8319 100644 --- a/apps/web/src/lib/kilo-pass/cancel-and-refund.ts +++ b/apps/web/src/lib/kilo-pass/cancel-and-refund.ts @@ -178,6 +178,16 @@ export async function cancelAndRefundKiloPassForUser({ } const balanceResetAmountUsd = await db.transaction(async tx => { + const freshUserRows = await tx + .select({ + microdollars_used: kilocode_users.microdollars_used, + total_microdollars_acquired: kilocode_users.total_microdollars_acquired, + }) + .from(kilocode_users) + .where(eq(kilocode_users.id, userId)) + .for('update') + .limit(1); + await tx .update(kilo_pass_subscriptions) .set({ @@ -198,16 +208,6 @@ export async function cancelAndRefundKiloPassForUser({ }) .where(eq(kilocode_users.id, userId)); } - - const freshUserRows = await tx - .select({ - microdollars_used: kilocode_users.microdollars_used, - total_microdollars_acquired: kilocode_users.total_microdollars_acquired, - }) - .from(kilocode_users) - .where(eq(kilocode_users.id, userId)) - .for('update') - .limit(1); const freshUser = freshUserRows[0]; const currentBalanceMicrodollars = freshUser ? freshUser.total_microdollars_acquired - freshUser.microdollars_used diff --git a/apps/web/src/lib/kilo-pass/state.ts b/apps/web/src/lib/kilo-pass/state.ts index 97c86161fa..cb76acdff5 100644 --- a/apps/web/src/lib/kilo-pass/state.ts +++ b/apps/web/src/lib/kilo-pass/state.ts @@ -122,9 +122,10 @@ function pickSubscriptionForState( export async function getKiloPassStateForUser( db: DbOrTx, - kiloUserId: string + kiloUserId: string, + options: { forUpdate?: boolean } = {} ): Promise { - const subscriptions = await db + const subscriptionQuery = db .select({ subscriptionId: kilo_pass_subscriptions.id, stripeSubscriptionId: kilo_pass_subscriptions.stripe_subscription_id, @@ -141,6 +142,9 @@ export async function getKiloPassStateForUser( }) .from(kilo_pass_subscriptions) .where(eq(kilo_pass_subscriptions.kilo_user_id, kiloUserId)); + const subscriptions = options.forUpdate + ? await subscriptionQuery.for('update') + : await subscriptionQuery; const selected = pickSubscriptionForState(subscriptions); if (!selected) return null; diff --git a/apps/web/src/lib/kiloclaw/credit-billing.ts b/apps/web/src/lib/kiloclaw/credit-billing.ts index a9091c1900..5a7e0bc58b 100644 --- a/apps/web/src/lib/kiloclaw/credit-billing.ts +++ b/apps/web/src/lib/kiloclaw/credit-billing.ts @@ -1,6 +1,6 @@ import 'server-only'; -import { and, desc, eq, isNotNull, isNull, sql } from 'drizzle-orm'; +import { and, desc, eq, gte, isNotNull, isNull, like, lte, ne, sql } from 'drizzle-orm'; import { addMonths, format } from 'date-fns'; import { db } from '@/lib/drizzle'; @@ -638,6 +638,7 @@ export async function applyStripeFundedKiloClawPeriod( } const commitEndsAt = plan === 'commit' ? targetRow.commit_ends_at : null; + const deductionCategory = `kiloclaw-settlement:${stripeSubscriptionId}:payment:${stripePaymentId}`; const deposited = await processTopUp( user, amountCents, @@ -650,43 +651,105 @@ export async function applyStripeFundedKiloClawPeriod( ); if (!deposited) { - logInfo('Duplicate settlement credit skipped', { + const [matchingPaymentDeduction] = await tx + .select({ id: credit_transactions.id }) + .from(credit_transactions) + .where( + and( + eq(credit_transactions.kilo_user_id, userId), + eq(credit_transactions.credit_category, deductionCategory) + ) + ) + .limit(1); + const periodSettlementDeposits = matchingPaymentDeduction + ? [] + : await tx + .select({ id: credit_transactions.id }) + .from(credit_transactions) + .where( + and( + eq(credit_transactions.kilo_user_id, userId), + eq(credit_transactions.amount_microdollars, amountMicrodollars), + eq(credit_transactions.description, `KiloClaw ${plan} settlement`), + gte(credit_transactions.created_at, periodStart), + lte(credit_transactions.created_at, periodEnd) + ) + ); + const periodSettlementDeductions = matchingPaymentDeduction + ? [] + : await tx + .select({ id: credit_transactions.id }) + .from(credit_transactions) + .where( + and( + eq(credit_transactions.kilo_user_id, userId), + eq(credit_transactions.amount_microdollars, -amountMicrodollars), + like( + credit_transactions.credit_category, + `kiloclaw-settlement:${stripeSubscriptionId}:%` + ), + gte(credit_transactions.created_at, periodStart), + lte(credit_transactions.created_at, periodEnd) + ) + ); + const hasUnmatchedDeposit = + !matchingPaymentDeduction && + periodSettlementDeposits.length > periodSettlementDeductions.length; + + if (hasUnmatchedDeposit) { + await tx.insert(credit_transactions).values({ + id: crypto.randomUUID(), + kilo_user_id: userId, + amount_microdollars: -amountMicrodollars, + is_free: false, + description: `KiloClaw ${plan} period deduction`, + credit_category: deductionCategory, + check_category_uniqueness: true, + original_baseline_microdollars_used: user.microdollars_used, + }); + await tx + .update(kilocode_users) + .set({ + total_microdollars_acquired: sql`${kilocode_users.total_microdollars_acquired} - ${amountMicrodollars}`, + }) + .where(eq(kilocode_users.id, userId)); + } + + logInfo('Duplicate settlement credit reconciled', { user_id: userId, stripe_payment_id: stripePaymentId, + reconciled_unmatched_deposit: hasUnmatchedDeposit, }); - applied = true; settlementWasDuplicate = true; - return; - } - - const deductionCategory = `kiloclaw-settlement:${stripeSubscriptionId}:payment:${stripePaymentId}`; - const deductionResult = await tx - .insert(credit_transactions) - .values({ - id: crypto.randomUUID(), - kilo_user_id: userId, - amount_microdollars: -amountMicrodollars, - is_free: false, - description: `KiloClaw ${plan} period deduction`, - credit_category: deductionCategory, - check_category_uniqueness: true, - original_baseline_microdollars_used: user.microdollars_used, - }) - .onConflictDoNothing(); + } else { + const deductionResult = await tx + .insert(credit_transactions) + .values({ + id: crypto.randomUUID(), + kilo_user_id: userId, + amount_microdollars: -amountMicrodollars, + is_free: false, + description: `KiloClaw ${plan} period deduction`, + credit_category: deductionCategory, + check_category_uniqueness: true, + original_baseline_microdollars_used: user.microdollars_used, + }) + .onConflictDoNothing(); + + if ((deductionResult.rowCount ?? 0) === 0) { + throw new Error( + `Stripe-funded KiloClaw settlement deduction conflict for payment ${stripePaymentId}` + ); + } - if ((deductionResult.rowCount ?? 0) === 0) { - throw new Error( - `Stripe-funded KiloClaw settlement deduction conflict for payment ${stripePaymentId}` - ); + await tx + .update(kilocode_users) + .set({ + total_microdollars_acquired: sql`${kilocode_users.total_microdollars_acquired} - ${amountMicrodollars}`, + }) + .where(eq(kilocode_users.id, userId)); } - await tx - .update(kilocode_users) - .set({ - total_microdollars_acquired: sql`${kilocode_users.total_microdollars_acquired} - ${amountMicrodollars}`, - }) - .where(eq(kilocode_users.id, userId)); - const updateSet = { instance_id: targetRow.instance_id, stripe_subscription_id: stripeSubscriptionId, @@ -1227,7 +1290,7 @@ export async function enrollWithCredits(params: { actor?: KiloClawSubscriptionChangeActor; commitQualification?: KiloClawCommitEnrollmentQualification; }): Promise { - const { userId, instanceId, plan, hadPaidSubscription } = params; + const { userId, instanceId, plan } = params; try { assertKiloClawCommitAdmission({ plan, qualification: params.commitQualification }); } catch (error) { @@ -1288,54 +1351,7 @@ export async function enrollWithCredits(params: { ); } - const isLiveTrialLineage = existingSub?.status === 'trialing'; - if ( - existingSub?.status === 'trialing' && - !isKiloClawPriceVersion(existingSub.kiloclaw_price_version) - ) { - throw new CreditEnrollmentError( - 'unknown_price_version', - `Unknown KiloClaw price version: ${existingSub.kiloclaw_price_version}` - ); - } - const kiloclawPriceVersion = resolveKiloClawEnrollmentPriceVersion( - existingSub - ? { - status: existingSub.status, - kiloclawPriceVersion: existingSub.kiloclaw_price_version, - } - : null - ); - if (params.expectedPriceVersion && params.expectedPriceVersion !== kiloclawPriceVersion) { - throw new CreditEnrollmentError( - 'price_version_mismatch', - `Credit enrollment intent price version no longer matches target: intended=${params.expectedPriceVersion} expected=${kiloclawPriceVersion}` - ); - } - const kiloclawPricing = getKiloClawPricingCatalogEntry(kiloclawPriceVersion); - - // First-time standard-plan subscribers in an eligible live pre-rollout lineage - // get that version's intro price. Current and canceled-history enrollments use - // recurring pricing from the first paid period. Commit has no intro discount. - // See spec Credit Enrollment rule 3. - const useStandardIntro = - isLiveTrialLineage && - plan === 'standard' && - kiloclawPricing.standardIntroMicrodollars !== undefined && - !hadPaidSubscription; - const costMicrodollars = getKiloClawPlanCostMicrodollars({ - priceVersion: kiloclawPriceVersion, - plan, - useStandardIntro, - }); - const saleItemSku = useStandardIntro - ? getStripePriceIdForClawPlanIntro('standard', { priceVersion: kiloclawPriceVersion }) - : getStripePriceIdForClawPlan(plan, { priceVersion: kiloclawPriceVersion }); - - // Save suspension state for post-transaction auto-resume (spec rule 4) - const wasSuspended = !!existingSub?.suspended_at; - - // Step 2: Serialize the balance decision and enrollment mutation. + // Step 2: Serialize the pricing, balance decision, and enrollment mutation. const now = new Date(); const periodMonths = plan === 'commit' ? 6 : 1; const periodEnd = addMonths(now, periodMonths); @@ -1350,7 +1366,12 @@ export async function enrollWithCredits(params: { const saleDedupeKeyEntityId = deductionCategory; let deductionWasDuplicate = false; - const trialEndEntityId = existingSub?.status === 'trialing' ? existingSub.id : undefined; + let costMicrodollars = 0; + let kiloclawPriceVersion: KiloClawPriceVersion | undefined; + let saleItemSku: string | undefined; + let trialEndEntityId: string | undefined; + let transitionedFromTrial = false; + let wasSuspended = false; await db.transaction(async tx => { const [user] = await tx @@ -1397,23 +1418,77 @@ export async function enrollWithCredits(params: { isNull(kiloclaw_subscriptions.transferred_to_subscription_id) ) ) + .for('update') .limit(1); + + if ( + currentSubscription && + currentSubscription.status !== 'trialing' && + currentSubscription.status !== 'canceled' + ) { + throw new CreditEnrollmentError( + 'active_subscription_exists', + 'Cannot enroll: an active subscription already exists. Cancel it first.' + ); + } if ( - currentSubscription?.id !== existingSub?.id || - currentSubscription?.status !== existingSub?.status || - currentSubscription?.kiloclaw_price_version !== existingSub?.kiloclaw_price_version + currentSubscription?.status === 'trialing' && + !isKiloClawPriceVersion(currentSubscription.kiloclaw_price_version) ) { throw new CreditEnrollmentError( - 'target_changed', - 'Credit enrollment target changed before the deduction could be applied.' + 'unknown_price_version', + `Unknown KiloClaw price version: ${currentSubscription.kiloclaw_price_version}` + ); + } + + kiloclawPriceVersion = resolveKiloClawEnrollmentPriceVersion( + currentSubscription + ? { + status: currentSubscription.status, + kiloclawPriceVersion: currentSubscription.kiloclaw_price_version, + } + : null + ); + if (params.expectedPriceVersion && params.expectedPriceVersion !== kiloclawPriceVersion) { + throw new CreditEnrollmentError( + 'price_version_mismatch', + `Credit enrollment intent price version no longer matches target: intended=${params.expectedPriceVersion} expected=${kiloclawPriceVersion}` ); } + const [paidSubscription] = await tx + .select({ id: kiloclaw_subscriptions.id }) + .from(kiloclaw_subscriptions) + .where( + and(eq(kiloclaw_subscriptions.user_id, userId), ne(kiloclaw_subscriptions.plan, 'trial')) + ) + .limit(1); + const currentHadPaidSubscription = paidSubscription !== undefined; + const kiloclawPricing = getKiloClawPricingCatalogEntry(kiloclawPriceVersion); + const useStandardIntro = + currentSubscription?.status === 'trialing' && + plan === 'standard' && + kiloclawPricing.standardIntroMicrodollars !== undefined && + !currentHadPaidSubscription; + costMicrodollars = getKiloClawPlanCostMicrodollars({ + priceVersion: kiloclawPriceVersion, + plan, + useStandardIntro, + }); + saleItemSku = useStandardIntro + ? getStripePriceIdForClawPlanIntro('standard', { priceVersion: kiloclawPriceVersion }) + : getStripePriceIdForClawPlan(plan, { priceVersion: kiloclawPriceVersion }); + transitionedFromTrial = + currentSubscription?.status === 'trialing' && currentSubscription.plan === 'trial'; + trialEndEntityId = + currentSubscription?.status === 'trialing' ? currentSubscription.id : undefined; + wasSuspended = !!currentSubscription?.suspended_at; + const projectedUsage = user.microdollars_used + costMicrodollars; const effectiveThreshold = getEffectiveKiloPassThreshold(user.kilo_pass_threshold); const kiloPassSubscription = effectiveThreshold !== null && projectedUsage >= effectiveThreshold - ? await getKiloPassStateForUser(tx, userId) + ? await getKiloPassStateForUser(tx, userId, { forUpdate: true }) : null; const balance = user.total_microdollars_acquired - user.microdollars_used; const { effectiveBalanceMicrodollars: effectiveBalance } = @@ -1463,8 +1538,8 @@ export async function enrollWithCredits(params: { } if ( - existingSub?.status === 'canceled' && - existingSub.kiloclaw_price_version !== kiloclawPriceVersion + currentSubscription?.status === 'canceled' && + currentSubscription.kiloclaw_price_version !== kiloclawPriceVersion ) { throw new CreditEnrollmentError( 'requires_reprovision', @@ -1543,6 +1618,10 @@ export async function enrollWithCredits(params: { } }); + if (!kiloclawPriceVersion || !saleItemSku) { + throw new Error('Credit enrollment completed without resolved pricing'); + } + if (deductionWasDuplicate) { try { await enqueueCreditEnrollmentAffiliateEvents({ @@ -1606,7 +1685,7 @@ export async function enrollWithCredits(params: { } // Step 5: Auto-resume if suspended (spec rule 7) - if (existingSub?.plan === 'trial' && existingSub.status === 'trialing') { + if (transitionedFromTrial) { try { await clearTrialInactivityStopAfterTrialTransition({ kiloUserId: userId, diff --git a/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts b/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts index 725218c242..0de5adb211 100644 --- a/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts +++ b/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts @@ -650,6 +650,103 @@ describe('Stripe-funded KiloClaw settlement', () => { ).toBe(2); }); + it('reconciles a duplicate payment deposit not covered by a legacy period deduction', async () => { + const user = await insertTestUser({ id: 'settlement-legacy-deduction-reconcile-user' }); + const instanceId = '78787878-7878-4787-8787-787878787878'; + const subscriptionId = '79797979-7979-4797-8797-797979797979'; + const stripeSubscriptionId = 'sub_legacy_deduction_reconcile'; + const firstPaymentId = 'ch_legacy_deduction_first'; + const replayedPaymentId = 'ch_legacy_deduction_second'; + const periodStart = '2026-06-01T00:00:00.000Z'; + const periodEnd = '2026-07-01T00:00:00.000Z'; + + await insertPersonalInstance({ id: instanceId, userId: user.id }); + await db.insert(kiloclaw_subscriptions).values({ + id: subscriptionId, + user_id: user.id, + instance_id: instanceId, + stripe_subscription_id: stripeSubscriptionId, + payment_source: 'credits', + kiloclaw_price_version: CURRENT_KILOCLAW_PRICE_VERSION, + plan: 'standard', + status: 'past_due', + current_period_start: '2026-05-01T00:00:00.000Z', + current_period_end: periodStart, + credit_renewal_at: periodStart, + past_due_since: periodStart, + }); + await db.insert(credit_transactions).values([ + { + kilo_user_id: user.id, + amount_microdollars: 55_000_000, + is_free: false, + stripe_payment_id: firstPaymentId, + description: 'KiloClaw standard settlement', + created_at: '2026-06-01T00:00:00.000Z', + }, + { + kilo_user_id: user.id, + amount_microdollars: 55_000_000, + is_free: false, + stripe_payment_id: replayedPaymentId, + description: 'KiloClaw standard settlement', + created_at: '2026-06-01T00:00:02.000Z', + }, + { + kilo_user_id: user.id, + amount_microdollars: -55_000_000, + is_free: false, + credit_category: `kiloclaw-settlement:${stripeSubscriptionId}:2026-06-01`, + check_category_uniqueness: true, + created_at: '2026-06-01T00:00:01.000Z', + }, + ]); + await db + .update(kilocode_users) + .set({ total_microdollars_acquired: 55_000_000 }) + .where(eq(kilocode_users.id, user.id)); + + await expect( + applyStripeFundedKiloClawPeriod({ + userId: user.id, + metadataInstanceId: instanceId, + stripeSubscriptionId, + stripePaymentId: replayedPaymentId, + plan: 'standard', + priceVersion: CURRENT_KILOCLAW_PRICE_VERSION, + amountMicrodollars: 55_000_000, + periodStart, + periodEnd, + }) + ).resolves.toBe(true); + + await expect(readUser(user.id)).resolves.toMatchObject({ + total_microdollars_acquired: 0, + }); + await expect(readSubscription(subscriptionId)).resolves.toMatchObject({ + status: 'active', + current_period_start: '2026-06-01 00:00:00+00', + current_period_end: '2026-07-01 00:00:00+00', + past_due_since: null, + }); + + const transactions = await db + .select({ + amountMicrodollars: credit_transactions.amount_microdollars, + creditCategory: credit_transactions.credit_category, + }) + .from(credit_transactions) + .where(eq(credit_transactions.kilo_user_id, user.id)); + expect(transactions.map(row => row.amountMicrodollars).sort((a, b) => a - b)).toEqual([ + -55_000_000, -55_000_000, 55_000_000, 55_000_000, + ]); + expect(transactions).toContainEqual( + expect.objectContaining({ + creditCategory: `kiloclaw-settlement:${stripeSubscriptionId}:payment:${replayedPaymentId}`, + }) + ); + }); + it('rolls back the deposit and subscription mutation when its deduction cannot be recorded', async () => { const user = await insertTestUser({ id: 'settlement-deduction-conflict-user' }); const instanceId = '89898989-8989-4898-8989-898989898989'; diff --git a/apps/web/src/routers/kiloclaw-billing-router.test.ts b/apps/web/src/routers/kiloclaw-billing-router.test.ts index 58ca2be272..c1690a3b8a 100644 --- a/apps/web/src/routers/kiloclaw-billing-router.test.ts +++ b/apps/web/src/routers/kiloclaw-billing-router.test.ts @@ -7584,6 +7584,68 @@ describe('enrollWithCredits', () => { ); }); + it('serializes intro eligibility so only one concurrent legacy trial enrollment is discounted', async () => { + await giveUserCredits(user.id, 13_000_000); + const [firstInstance, secondInstance] = await db + .insert(kiloclaw_instances) + .values([ + { + user_id: user.id, + sandbox_id: `ki_concurrent_intro_first_${user.id}`, + }, + { + user_id: user.id, + sandbox_id: `ki_concurrent_intro_second_${user.id}`, + }, + ]) + .returning(); + await db.insert(kiloclaw_subscriptions).values([ + { + user_id: user.id, + instance_id: firstInstance.id, + plan: 'trial', + status: 'trialing', + kiloclaw_price_version: LEGACY_KILOCLAW_PRICE_VERSION, + }, + { + user_id: user.id, + instance_id: secondInstance.id, + plan: 'trial', + status: 'trialing', + kiloclaw_price_version: LEGACY_KILOCLAW_PRICE_VERSION, + }, + ]); + + const { enrollWithCredits } = await import('@/lib/kiloclaw/credit-billing'); + await expect( + Promise.all([ + enrollWithCredits({ + userId: user.id, + instanceId: firstInstance.id, + plan: 'standard', + hadPaidSubscription: false, + }), + enrollWithCredits({ + userId: user.id, + instanceId: secondInstance.id, + plan: 'standard', + hadPaidSubscription: false, + }), + ]) + ).resolves.toEqual([undefined, undefined]); + + const deductions = await db + .select({ amountMicrodollars: credit_transactions.amount_microdollars }) + .from(credit_transactions) + .where(eq(credit_transactions.kilo_user_id, user.id)); + expect( + deductions + .filter(row => row.amountMicrodollars < 0) + .map(row => row.amountMicrodollars) + .sort((a, b) => a - b) + ).toEqual([-9_000_000, -4_000_000]); + }); + it('serializes concurrent enrollment balance decisions for different instances', async () => { await giveUserCredits(user.id, 55_000_000); const [firstInstance, secondInstance] = await db From 0c4692b451b5f18e755d93ad01a3f3975ca583ba Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Mon, 22 Jun 2026 20:52:06 +0200 Subject: [PATCH 3/4] fix(kiloclaw): scope settlement reconciliation --- .../src/lib/kilo-pass/cancel-and-refund.ts | 20 ++-- apps/web/src/lib/kiloclaw/credit-billing.ts | 46 ++++---- .../kiloclaw/stripe-funded-settlement.test.ts | 110 ++++++++++++++++++ 3 files changed, 142 insertions(+), 34 deletions(-) diff --git a/apps/web/src/lib/kilo-pass/cancel-and-refund.ts b/apps/web/src/lib/kilo-pass/cancel-and-refund.ts index 3ffb8c8319..2141359fde 100644 --- a/apps/web/src/lib/kilo-pass/cancel-and-refund.ts +++ b/apps/web/src/lib/kilo-pass/cancel-and-refund.ts @@ -178,16 +178,6 @@ export async function cancelAndRefundKiloPassForUser({ } const balanceResetAmountUsd = await db.transaction(async tx => { - const freshUserRows = await tx - .select({ - microdollars_used: kilocode_users.microdollars_used, - total_microdollars_acquired: kilocode_users.total_microdollars_acquired, - }) - .from(kilocode_users) - .where(eq(kilocode_users.id, userId)) - .for('update') - .limit(1); - await tx .update(kilo_pass_subscriptions) .set({ @@ -208,6 +198,16 @@ export async function cancelAndRefundKiloPassForUser({ }) .where(eq(kilocode_users.id, userId)); } + + const freshUserRows = await tx + .select({ + microdollars_used: kilocode_users.microdollars_used, + total_microdollars_acquired: kilocode_users.total_microdollars_acquired, + }) + .from(kilocode_users) + .where(eq(kilocode_users.id, userId)) + .for('update') + .limit(1); const freshUser = freshUserRows[0]; const currentBalanceMicrodollars = freshUser ? freshUser.total_microdollars_acquired - freshUser.microdollars_used diff --git a/apps/web/src/lib/kiloclaw/credit-billing.ts b/apps/web/src/lib/kiloclaw/credit-billing.ts index 5a7e0bc58b..0a4bccf0f1 100644 --- a/apps/web/src/lib/kiloclaw/credit-billing.ts +++ b/apps/web/src/lib/kiloclaw/credit-billing.ts @@ -1,6 +1,6 @@ import 'server-only'; -import { and, desc, eq, gte, isNotNull, isNull, like, lte, ne, sql } from 'drizzle-orm'; +import { and, desc, eq, isNotNull, isNull, ne, sql } from 'drizzle-orm'; import { addMonths, format } from 'date-fns'; import { db } from '@/lib/drizzle'; @@ -661,7 +661,7 @@ export async function applyStripeFundedKiloClawPeriod( ) ) .limit(1); - const periodSettlementDeposits = matchingPaymentDeduction + const [legacyPeriodDeduction] = matchingPaymentDeduction ? [] : await tx .select({ id: credit_transactions.id }) @@ -669,34 +669,32 @@ export async function applyStripeFundedKiloClawPeriod( .where( and( eq(credit_transactions.kilo_user_id, userId), - eq(credit_transactions.amount_microdollars, amountMicrodollars), - eq(credit_transactions.description, `KiloClaw ${plan} settlement`), - gte(credit_transactions.created_at, periodStart), - lte(credit_transactions.created_at, periodEnd) + eq( + credit_transactions.credit_category, + `kiloclaw-settlement:${stripeSubscriptionId}:${periodStart.slice(0, 10)}` + ) ) - ); - const periodSettlementDeductions = matchingPaymentDeduction + ) + .limit(1); + const [settledSamePeriod] = matchingPaymentDeduction ? [] : await tx - .select({ id: credit_transactions.id }) - .from(credit_transactions) + .select({ id: kiloclaw_subscription_change_log.id }) + .from(kiloclaw_subscription_change_log) .where( and( - eq(credit_transactions.kilo_user_id, userId), - eq(credit_transactions.amount_microdollars, -amountMicrodollars), - like( - credit_transactions.credit_category, - `kiloclaw-settlement:${stripeSubscriptionId}:%` - ), - gte(credit_transactions.created_at, periodStart), - lte(credit_transactions.created_at, periodEnd) + eq(kiloclaw_subscription_change_log.subscription_id, targetRow.id), + eq(kiloclaw_subscription_change_log.action, 'period_advanced'), + eq(kiloclaw_subscription_change_log.reason, 'stripe_invoice_settlement'), + sql`${kiloclaw_subscription_change_log.after_state}->>'current_period_start' = ${periodStart}`, + sql`${kiloclaw_subscription_change_log.after_state}->>'current_period_end' = ${periodEnd}` ) - ); - const hasUnmatchedDeposit = - !matchingPaymentDeduction && - periodSettlementDeposits.length > periodSettlementDeductions.length; + ) + .limit(1); + const shouldReconcilePaymentDeduction = + !matchingPaymentDeduction && (!legacyPeriodDeduction || !!settledSamePeriod); - if (hasUnmatchedDeposit) { + if (shouldReconcilePaymentDeduction) { await tx.insert(credit_transactions).values({ id: crypto.randomUUID(), kilo_user_id: userId, @@ -718,7 +716,7 @@ export async function applyStripeFundedKiloClawPeriod( logInfo('Duplicate settlement credit reconciled', { user_id: userId, stripe_payment_id: stripePaymentId, - reconciled_unmatched_deposit: hasUnmatchedDeposit, + reconciled_payment_deduction: shouldReconcilePaymentDeduction, }); settlementWasDuplicate = true; } else { diff --git a/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts b/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts index 0de5adb211..cd3b77eec8 100644 --- a/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts +++ b/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts @@ -5,6 +5,7 @@ import { CURRENT_KILOCLAW_PRICE_VERSION, LEGACY_KILOCLAW_PRICE_VERSION } from '@ import { credit_transactions, kiloclaw_instances, + kiloclaw_subscription_change_log, kiloclaw_subscriptions, kilocode_users, } from '@kilocode/db/schema'; @@ -701,6 +702,18 @@ describe('Stripe-funded KiloClaw settlement', () => { created_at: '2026-06-01T00:00:01.000Z', }, ]); + await db.insert(kiloclaw_subscription_change_log).values({ + subscription_id: subscriptionId, + actor_type: 'system', + actor_id: 'kiloclaw-credit-billing', + action: 'period_advanced', + reason: 'stripe_invoice_settlement', + before_state: null, + after_state: { + current_period_start: periodStart, + current_period_end: periodEnd, + }, + }); await db .update(kilocode_users) .set({ total_microdollars_acquired: 55_000_000 }) @@ -747,6 +760,103 @@ describe('Stripe-funded KiloClaw settlement', () => { ); }); + it('does not use another subscription deposit to reconcile a legacy-covered replay', async () => { + const user = await insertTestUser({ id: 'settlement-cross-subscription-user' }); + const firstInstanceId = '81818181-8181-4181-8181-818181818181'; + const secondInstanceId = '82828282-8282-4282-8282-828282828282'; + const firstSubscriptionId = '83838383-8383-4383-8383-838383838383'; + const secondSubscriptionId = '84848484-8484-4484-8484-848484848484'; + const firstStripeSubscriptionId = 'sub_cross_subscription_first'; + const secondStripeSubscriptionId = 'sub_cross_subscription_second'; + const periodStart = '2026-06-01T00:00:00.000Z'; + const periodEnd = '2026-07-01T00:00:00.000Z'; + + await insertPersonalInstance({ id: firstInstanceId, userId: user.id }); + await insertPersonalInstance({ id: secondInstanceId, userId: user.id }); + await db.insert(kiloclaw_subscriptions).values([ + { + id: firstSubscriptionId, + user_id: user.id, + instance_id: firstInstanceId, + stripe_subscription_id: firstStripeSubscriptionId, + payment_source: 'credits', + kiloclaw_price_version: CURRENT_KILOCLAW_PRICE_VERSION, + plan: 'standard', + status: 'active', + current_period_start: periodStart, + current_period_end: periodEnd, + credit_renewal_at: periodEnd, + }, + { + id: secondSubscriptionId, + user_id: user.id, + instance_id: secondInstanceId, + stripe_subscription_id: secondStripeSubscriptionId, + payment_source: 'credits', + kiloclaw_price_version: CURRENT_KILOCLAW_PRICE_VERSION, + plan: 'standard', + status: 'past_due', + current_period_start: '2026-05-01T00:00:00.000Z', + current_period_end: periodStart, + credit_renewal_at: periodStart, + }, + ]); + await db.insert(credit_transactions).values([ + { + kilo_user_id: user.id, + amount_microdollars: 55_000_000, + is_free: false, + stripe_payment_id: 'ch_cross_subscription_other', + description: 'KiloClaw standard settlement', + }, + { + kilo_user_id: user.id, + amount_microdollars: 55_000_000, + is_free: false, + stripe_payment_id: 'ch_cross_subscription_replay', + description: 'KiloClaw standard settlement', + }, + { + kilo_user_id: user.id, + amount_microdollars: -55_000_000, + is_free: false, + credit_category: `kiloclaw-settlement:${firstStripeSubscriptionId}:payment:ch_cross_subscription_other`, + check_category_uniqueness: true, + }, + { + kilo_user_id: user.id, + amount_microdollars: -55_000_000, + is_free: false, + credit_category: `kiloclaw-settlement:${secondStripeSubscriptionId}:2026-06-01`, + check_category_uniqueness: true, + }, + ]); + + await expect( + applyStripeFundedKiloClawPeriod({ + userId: user.id, + metadataInstanceId: secondInstanceId, + stripeSubscriptionId: secondStripeSubscriptionId, + stripePaymentId: 'ch_cross_subscription_replay', + plan: 'standard', + priceVersion: CURRENT_KILOCLAW_PRICE_VERSION, + amountMicrodollars: 55_000_000, + periodStart, + periodEnd, + }) + ).resolves.toBe(true); + + const deductions = await db + .select({ creditCategory: credit_transactions.credit_category }) + .from(credit_transactions) + .where(eq(credit_transactions.kilo_user_id, user.id)); + expect(deductions).not.toContainEqual( + expect.objectContaining({ + creditCategory: `kiloclaw-settlement:${secondStripeSubscriptionId}:payment:ch_cross_subscription_replay`, + }) + ); + }); + it('rolls back the deposit and subscription mutation when its deduction cannot be recorded', async () => { const user = await insertTestUser({ id: 'settlement-deduction-conflict-user' }); const instanceId = '89898989-8989-4898-8989-898989898989'; From a29d2baece1a868d9c31ca63785c23096f869d44 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Mon, 22 Jun 2026 21:06:27 +0200 Subject: [PATCH 4/4] fix(kiloclaw): avoid legacy settlement double deduction --- apps/web/src/lib/kiloclaw/credit-billing.ts | 2 +- .../kiloclaw/stripe-funded-settlement.test.ts | 75 ++++++++++++++++++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/apps/web/src/lib/kiloclaw/credit-billing.ts b/apps/web/src/lib/kiloclaw/credit-billing.ts index 0a4bccf0f1..1fc9335ed9 100644 --- a/apps/web/src/lib/kiloclaw/credit-billing.ts +++ b/apps/web/src/lib/kiloclaw/credit-billing.ts @@ -692,7 +692,7 @@ export async function applyStripeFundedKiloClawPeriod( ) .limit(1); const shouldReconcilePaymentDeduction = - !matchingPaymentDeduction && (!legacyPeriodDeduction || !!settledSamePeriod); + !matchingPaymentDeduction && !legacyPeriodDeduction && !settledSamePeriod; if (shouldReconcilePaymentDeduction) { await tx.insert(credit_transactions).values({ diff --git a/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts b/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts index cd3b77eec8..ef772caa87 100644 --- a/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts +++ b/apps/web/src/lib/kiloclaw/stripe-funded-settlement.test.ts @@ -651,7 +651,7 @@ describe('Stripe-funded KiloClaw settlement', () => { ).toBe(2); }); - it('reconciles a duplicate payment deposit not covered by a legacy period deduction', async () => { + it('does not guess which duplicate payment is covered by a legacy period deduction', async () => { const user = await insertTestUser({ id: 'settlement-legacy-deduction-reconcile-user' }); const instanceId = '78787878-7878-4787-8787-787878787878'; const subscriptionId = '79797979-7979-4797-8797-797979797979'; @@ -734,7 +734,7 @@ describe('Stripe-funded KiloClaw settlement', () => { ).resolves.toBe(true); await expect(readUser(user.id)).resolves.toMatchObject({ - total_microdollars_acquired: 0, + total_microdollars_acquired: 55_000_000, }); await expect(readSubscription(subscriptionId)).resolves.toMatchObject({ status: 'active', @@ -751,15 +751,82 @@ describe('Stripe-funded KiloClaw settlement', () => { .from(credit_transactions) .where(eq(credit_transactions.kilo_user_id, user.id)); expect(transactions.map(row => row.amountMicrodollars).sort((a, b) => a - b)).toEqual([ - -55_000_000, -55_000_000, 55_000_000, 55_000_000, + -55_000_000, 55_000_000, 55_000_000, ]); - expect(transactions).toContainEqual( + expect(transactions).not.toContainEqual( expect.objectContaining({ creditCategory: `kiloclaw-settlement:${stripeSubscriptionId}:payment:${replayedPaymentId}`, }) ); }); + it('does not double-deduct a replay already covered by a legacy period deduction', async () => { + const user = await insertTestUser({ id: 'settlement-legacy-covered-replay-user' }); + const instanceId = '80808080-8080-4080-8080-808080808080'; + const subscriptionId = '85858585-8585-4585-8585-858585858585'; + const stripeSubscriptionId = 'sub_legacy_covered_replay'; + const stripePaymentId = 'ch_legacy_covered_replay'; + const periodStart = '2026-06-01T00:00:00.000Z'; + const periodEnd = '2026-07-01T00:00:00.000Z'; + + await insertPersonalInstance({ id: instanceId, userId: user.id }); + await db.insert(kiloclaw_subscriptions).values({ + id: subscriptionId, + user_id: user.id, + instance_id: instanceId, + stripe_subscription_id: stripeSubscriptionId, + payment_source: 'credits', + kiloclaw_price_version: CURRENT_KILOCLAW_PRICE_VERSION, + plan: 'standard', + status: 'active', + current_period_start: periodStart, + current_period_end: periodEnd, + credit_renewal_at: periodEnd, + }); + await db.insert(credit_transactions).values([ + { + kilo_user_id: user.id, + amount_microdollars: 55_000_000, + is_free: false, + stripe_payment_id: stripePaymentId, + description: 'KiloClaw standard settlement', + }, + { + kilo_user_id: user.id, + amount_microdollars: -55_000_000, + is_free: false, + credit_category: `kiloclaw-settlement:${stripeSubscriptionId}:2026-06-01`, + check_category_uniqueness: true, + }, + ]); + + await expect( + applyStripeFundedKiloClawPeriod({ + userId: user.id, + metadataInstanceId: instanceId, + stripeSubscriptionId, + stripePaymentId, + plan: 'standard', + priceVersion: CURRENT_KILOCLAW_PRICE_VERSION, + amountMicrodollars: 55_000_000, + periodStart, + periodEnd, + }) + ).resolves.toBe(true); + + const transactions = await db + .select({ creditCategory: credit_transactions.credit_category }) + .from(credit_transactions) + .where(eq(credit_transactions.kilo_user_id, user.id)); + expect(transactions).toHaveLength(2); + expect(transactions).not.toContainEqual( + expect.objectContaining({ creditCategory: expect.stringContaining(':payment:') }) + ); + await expect(readUser(user.id)).resolves.toMatchObject({ + total_microdollars_acquired: 0, + }); + }); + it('does not use another subscription deposit to reconcile a legacy-covered replay', async () => { const user = await insertTestUser({ id: 'settlement-cross-subscription-user' }); const firstInstanceId = '81818181-8181-4181-8181-818181818181';