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
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ export function KiloPassSubscribeCard(props: {
KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF.toISOString()
);
const monthlyPromoDescription =
cadence === KiloPassCadence.Monthly && showSecondMonthPromo
? `First-time subscribers receive 50% free bonus credits for the first two months. Offer expires ${promoCutoffLabel}.`
cadence === KiloPassCadence.Monthly && showFirstMonthPromo
? showSecondMonthPromo
? `First-time subscribers receive 50% free bonus credits for the first two months when they start before ${promoCutoffLabel}.`
: 'First-time subscribers receive 50% free bonus credits for the first month.'
: null;
const cadenceOptions = [
{ value: KiloPassCadence.Monthly, label: 'Monthly' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ export function KiloPassDetail() {
return promoPercent === KILO_PASS_FIRST_MONTH_PROMO_BONUS_PERCENT;
}, [subscription]);

const showSecondMonthPromoInDialog = useMemo(() => {
if (!subscription || subscription.cadence !== 'monthly') return false;
if (subscription.currentStreakMonths > 2) return false;
const month2Percent = computeMonthlyCadenceBonusPercent({
tier: subscription.tier,
streakMonths: 2,
isFirstTimeSubscriberEver: subscription.isFirstTimeSubscriberEver,
subscriptionStartedAtIso: subscription.startedAt,
});
return month2Percent === KILO_PASS_FIRST_MONTH_PROMO_BONUS_PERCENT;
}, [subscription]);

async function refreshData() {
await Promise.all([
queryClient.invalidateQueries({ queryKey: trpc.kiloPass.getState.queryKey() }),
Expand Down Expand Up @@ -236,7 +248,7 @@ export function KiloPassDetail() {
<KiloPassBonusRampDialog
tier={subscription.tier}
showFirstMonthPromo={showFirstMonthPromoInDialog}
showSecondMonthPromo={subscription.currentStreakMonths === 1}
showSecondMonthPromo={showSecondMonthPromoInDialog}
streakMonths={subscription.currentStreakMonths}
subscriptionStartedAtIso={subscription.startedAt ?? undefined}
/>
Expand Down
30 changes: 26 additions & 4 deletions apps/web/src/lib/kilo-pass/bonus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ describe('kilo pass bonus utilities', () => {
});

describe('computeMonthlyCadenceBonusPercent', () => {
it('keeps the second-month grandfather cutoff at midnight May 9 UTC', () => {
expect(KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF.toISOString()).toBe(
'2026-05-09T00:00:00.000Z'
);
});

it('applies the 50% promo for streak months 1 and 2 when eligible (strictly before cutoff)', () => {
expect(
computeMonthlyCadenceBonusPercent({
Expand Down Expand Up @@ -197,6 +203,19 @@ describe('kilo pass bonus utilities', () => {
);
});

it('applies the first-month promo for first-time subscribers after the grandfather cutoff', () => {
expect(
computeMonthlyCadenceBonusPercent({
tier: KiloPassTier.Tier19,
streakMonths: 1,
isFirstTimeSubscriberEver: true,
subscriptionStartedAtIso: new Date(
KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF.valueOf() + 1
).toISOString(),
})
).toBeCloseTo(KILO_PASS_FIRST_MONTH_PROMO_BONUS_PERCENT);
});

it('does not apply the override when isFirstTimeSubscriberEver is false', () => {
expect(
computeMonthlyCadenceBonusPercent({
Expand All @@ -223,26 +242,29 @@ describe('kilo pass bonus utilities', () => {
});
};

it('is ineligible at cutoff and returns the fallback value (month 1)', () => {
it('applies the first-month promo at the second-month grandfather cutoff', () => {
expect(
computeMonthlyCadenceBonusPercent({
tier,
streakMonths: 1,
isFirstTimeSubscriberEver: true,
subscriptionStartedAtIso: KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF.toISOString(),
})
).toBe(computeFallback({ streakMonths: 1, isFirstTimeSubscriberEver: true }));
).toBe(KILO_PASS_FIRST_MONTH_PROMO_BONUS_PERCENT);
});

it('is ineligible at cutoff and returns the fallback value (month 2)', () => {
it('does not apply the second-month promo at the grandfather cutoff', () => {
expect(
computeMonthlyCadenceBonusPercent({
tier,
streakMonths: 2,
isFirstTimeSubscriberEver: true,
subscriptionStartedAtIso: KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF.toISOString(),
})
).toBe(computeFallback({ streakMonths: 2, isFirstTimeSubscriberEver: true }));
).toBeCloseTo(
KILO_PASS_TIER_CONFIG.tier_49.monthlyBaseBonusPercent +
KILO_PASS_TIER_CONFIG.tier_49.monthlyStepBonusPercent
);
});

it('does not apply promo when isFirstTimeSubscriberEver is false', () => {
Expand Down
15 changes: 7 additions & 8 deletions apps/web/src/lib/kilo-pass/bonus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ export const computeMonthlyCadenceBonusPercent = (params: {
throw new Error('streakMonths must be >= 1');
}

// Limited-time promo: first-time subscribers who started strictly before the cutoff
// get a 50% bonus for streak months 1 and 2.
if (streakMonths <= 2 && isFirstTimeSubscriberEver) {
if (streakMonths === 1 && isFirstTimeSubscriberEver) {
return KILO_PASS_FIRST_MONTH_PROMO_BONUS_PERCENT;
}

// Limited-time grandfathered promo: first-time subscribers who started strictly before the
// cutoff keep the 50% bonus for streak month 2.
if (streakMonths === 2 && isFirstTimeSubscriberEver) {
const startedAt = subscriptionStartedAtIso ?? null;
if (startedAt != null) {
const startedAtUtc = dayjs(startedAt).utc();
Expand All @@ -50,11 +54,6 @@ export const computeMonthlyCadenceBonusPercent = (params: {
return KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_BONUS_PERCENT;
}
}

// Back-compat: if we don't have a start timestamp, still show the first month promo.
if (streakMonths === 1) {
return KILO_PASS_FIRST_MONTH_PROMO_BONUS_PERCENT;
}
}

const config = KILO_PASS_TIER_CONFIG[tier];
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/lib/kilo-pass/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ type KiloPassTierConfig = {

export const KILO_PASS_FIRST_MONTH_PROMO_BONUS_PERCENT = 0.5;

// First-time subscribers receive a 50% bonus for the first 2 months if they started
// strictly before this cutoff. (For PST, Incorporating DST)
export const KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF = dayjs('2026-05-07T15:53:43Z').utc();
// First-time subscribers receive a 50% bonus for month 2 only if they started
// strictly before this grandfather cutoff. Month 1 remains 50% for new subscribers.
export const KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF = dayjs('2026-05-09T00:00:00Z').utc();

export const KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_BONUS_PERCENT = 0.5;

Expand Down
33 changes: 12 additions & 21 deletions apps/web/src/routers/kilo-pass-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,7 @@ describe('kiloPassRouter', () => {

expect(result).toEqual({
subscription: null,
isEligibleForFirstMonthPromo:
new Date() < KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF.toDate(),
isEligibleForFirstMonthPromo: true,
});
});

Expand Down Expand Up @@ -744,28 +743,20 @@ describe('kiloPassRouter', () => {
const caller = await createCallerForUser(user.id);
const result = await caller.kiloPass.getState();

expect(result.isEligibleForFirstMonthPromo).toBe(
new Date() < KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF.toDate()
);
expect(result.isEligibleForFirstMonthPromo).toBe(true);
expect(result.subscription).toBeNull();
});

it('returns isEligibleForFirstMonthPromo=false after the promo cutoff', async () => {
// If the current time is at or after the cutoff, even a user with no subscriptions
// should see isEligibleForFirstMonthPromo=false.
const now = new Date();
const cutoff = KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF.toDate();
if (now >= cutoff) {
const user = await insertTestUser({
google_user_email: 'kilo-pass-promo-cutoff-ineligible@example.com',
});

const caller = await createCallerForUser(user.id);
const result = await caller.kiloPass.getState();

expect(result.isEligibleForFirstMonthPromo).toBe(false);
expect(result.subscription).toBeNull();
}
it('keeps isEligibleForFirstMonthPromo=true for a never-subscribed user', async () => {
const user = await insertTestUser({
google_user_email: 'kilo-pass-promo-cutoff-still-eligible@example.com',
});

const caller = await createCallerForUser(user.id);
const result = await caller.kiloPass.getState();

expect(result.isEligibleForFirstMonthPromo).toBe(true);
expect(result.subscription).toBeNull();
});

it('returns isEligibleForFirstMonthPromo=false when user has a canceled subscription', async () => {
Expand Down
36 changes: 10 additions & 26 deletions apps/web/src/routers/kilo-pass-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@ import { KiloPassError } from '@/lib/kilo-pass/errors';
import { isStripeSubscriptionEnded } from '@/lib/kilo-pass/stripe-subscription-status';
import { releaseScheduledChangeForSubscription } from '@/lib/kilo-pass/scheduled-change-release';
import { appendKiloPassAuditLog } from '@/lib/kilo-pass/issuance';
import {
KILO_PASS_FIRST_MONTH_PROMO_BONUS_PERCENT,
KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF,
KILO_PASS_TIER_CONFIG,
} from '@/lib/kilo-pass/constants';
import { KILO_PASS_TIER_CONFIG } from '@/lib/kilo-pass/constants';
import { fromMicrodollars } from '@/lib/utils';
import { timedUsageQuery } from '@/lib/usage-query';
import {
Expand Down Expand Up @@ -131,10 +127,6 @@ const GetAverageMonthlyUsageLast3MonthsOutputSchema = z.object({
averageMonthlyUsageUsd: z.number(),
});

function isTwoMonthPromoOfferActive(): boolean {
return dayjs().utc().isBefore(KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF);
}

function roundToCents(usd: number): number {
return Math.round(usd * 100) / 100;
}
Expand Down Expand Up @@ -384,7 +376,7 @@ export const kiloPassRouter = createTRPCRouter({
getState: baseProcedure.output(GetStateOutputSchema).query(async ({ ctx }) => {
const subscriptionBase = await getKiloPassStateForUser(db, ctx.user.id);
if (!subscriptionBase) {
return { subscription: null, isEligibleForFirstMonthPromo: isTwoMonthPromoOfferActive() };
return { subscription: null, isEligibleForFirstMonthPromo: true };
}

const stripeCustomerId = ctx.user.stripe_customer_id;
Expand Down Expand Up @@ -478,22 +470,14 @@ export const kiloPassRouter = createTRPCRouter({
currentPeriodBonusCreditsUsd = roundToCents(usd);
} else {
const streakMonths = Math.max(1, subscriptionBase.currentStreakMonths);
const shouldShowFirstMonthPromo =
streakMonths === 1 && isFirstTimeSubscriberEver && isTwoMonthPromoOfferActive();

if (shouldShowFirstMonthPromo) {
const cents = Math.round(baseAmountUsd * KILO_PASS_FIRST_MONTH_PROMO_BONUS_PERCENT * 100);
currentPeriodBonusCreditsUsd = cents / 100;
} else {
const bonusPercentApplied = computeMonthlyCadenceBonusPercent({
tier: subscriptionBase.tier,
streakMonths,
isFirstTimeSubscriberEver,
subscriptionStartedAtIso: subscriptionBase.startedAt,
});
const cents = Math.round(baseAmountUsd * bonusPercentApplied * 100);
currentPeriodBonusCreditsUsd = cents / 100;
}
const bonusPercentApplied = computeMonthlyCadenceBonusPercent({
tier: subscriptionBase.tier,
streakMonths,
isFirstTimeSubscriberEver,
subscriptionStartedAtIso: subscriptionBase.startedAt,
});
const cents = Math.round(baseAmountUsd * bonusPercentApplied * 100);
currentPeriodBonusCreditsUsd = cents / 100;
}

const nowUtc = dayjs().utc();
Expand Down