Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions apps/web/src/lib/kiloclaw/credit-billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,8 @@ export async function enrollWithCredits(params: {
const now = new Date();
const periodMonths = plan === 'commit' ? 6 : 1;
const periodEnd = addMonths(now, periodMonths);
const periodStartIso = now.toISOString();
const periodEndIso = periodEnd.toISOString();
const periodKey = format(now, 'yyyy-MM');
const categoryPrefix =
plan === 'commit'
Expand Down Expand Up @@ -1229,8 +1231,6 @@ export async function enrollWithCredits(params: {
.limit(1);

// 5c: Upsert subscription row as pure credit
const nowIso = now.toISOString();
const periodEndIso = periodEnd.toISOString();
const commitEndsAt = plan === 'commit' ? periodEndIso : null;
const [mutatedSubscription] = await tx
.insert(kiloclaw_subscriptions)
Expand All @@ -1240,7 +1240,7 @@ export async function enrollWithCredits(params: {
payment_source: 'credits',
status: 'active',
plan,
current_period_start: nowIso,
current_period_start: periodStartIso,
current_period_end: periodEndIso,
credit_renewal_at: periodEndIso,
stripe_subscription_id: null,
Expand All @@ -1259,7 +1259,7 @@ export async function enrollWithCredits(params: {
payment_source: 'credits',
status: 'active',
plan,
current_period_start: nowIso,
current_period_start: periodStartIso,
current_period_end: periodEndIso,
credit_renewal_at: periodEndIso,
stripe_subscription_id: null,
Expand Down Expand Up @@ -1355,6 +1355,17 @@ export async function enrollWithCredits(params: {
await autoResumeIfSuspended(userId, instanceId);
}

if (shouldSendSubscriptionStartedEmailForActivation(existingSub?.status ?? null)) {
await maybeSendKiloClawSubscriptionStartedEmail({
userId,
instanceId,
plan,
amountCents: Math.round(costMicrodollars / 10_000),
periodStart: periodStartIso,
periodEnd: periodEndIso,
});
}

logInfo('Credit enrollment completed', {
user_id: userId,
instanceId,
Expand Down
56 changes: 56 additions & 0 deletions apps/web/src/lib/purchase-emails.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
process.env.STRIPE_KILOCLAW_STANDARD_INTRO_PRICE_ID ||= 'price_standard_intro';

import { eq, and } from 'drizzle-orm';
import {
kiloclaw_email_log,
Expand Down Expand Up @@ -272,6 +274,25 @@ describe('applyStripeFundedKiloClawPeriod subscription-started email', () => {
return rows.length;
}

async function seedCreditEnrollmentAnchor(userId: string) {
const [instance] = await db
.insert(kiloclaw_instances)
.values({
user_id: userId,
sandbox_id: `test-sandbox-${crypto.randomUUID()}`,
})
.returning();
await db.insert(kiloclaw_subscriptions).values({
user_id: userId,
instance_id: instance.id,
plan: 'trial',
status: 'trialing',
trial_started_at: new Date().toISOString(),
trial_ends_at: new Date(Date.now() + 7 * 86_400_000).toISOString(),
});
return instance;
}

test('trialing trial → Stripe settlement sends one subscription-started email and writes the log row', async () => {
const user = await insertTestUser({});
const stripeSubscriptionId = `sub_trialing_${crypto.randomUUID()}`;
Expand All @@ -298,6 +319,41 @@ describe('applyStripeFundedKiloClawPeriod subscription-started email', () => {
expect(await countEmailLogRows(user.id, instance.id)).toBe(1);
});

test('trialing trial → credit enrollment sends one subscription-started email and writes the log row', async () => {
const user = await insertTestUser({ total_microdollars_acquired: 50_000_000 });
const instance = await seedCreditEnrollmentAnchor(user.id);

const mod = await import('@/lib/kiloclaw/credit-billing');
await mod.enrollWithCredits({
userId: user.id,
instanceId: instance.id,
plan: 'standard',
hadPaidSubscription: false,
});

expect(countSubscriptionStartedSends()).toBe(1);
expect(await countEmailLogRows(user.id, instance.id)).toBe(1);

const [subscription] = await db
.select()
.from(kiloclaw_subscriptions)
.where(eq(kiloclaw_subscriptions.instance_id, instance.id))
.limit(1);
const [emailLog] = await db
.select()
.from(kiloclaw_email_log)
.where(
and(
eq(kiloclaw_email_log.user_id, user.id),
eq(kiloclaw_email_log.instance_id, instance.id),
eq(kiloclaw_email_log.email_type, KILOCLAW_SUBSCRIPTION_STARTED_EMAIL_TYPE)
)
)
.limit(1);

expect(emailLog?.period_start).toBe(subscription.current_period_start);
});

test('canceled trial → Stripe settlement sends one subscription-started email', async () => {
const user = await insertTestUser({});
const stripeSubscriptionId = `sub_canceled_trial_${crypto.randomUUID()}`;
Expand Down