Skip to content

Commit ed5567d

Browse files
authored
refactor(billing): rename subscription bit to code_usage_subscribed
Matches the gateway field rename: the bit is the org's subscription status (payment method on file), and "billed" next to is_pro read ambiguously. Mechanical rename across the schema, gateway service, limit-content derivation, and billing UI; is_pro semantics untouched. Generated-By: PostHog Code Task-Id: bfbcd747-a365-429f-a5dc-622e2c2f7edf
1 parent c41f182 commit ed5567d

9 files changed

Lines changed: 60 additions & 56 deletions

File tree

packages/core/src/billing/usageDisplay.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ export function isUsageExceeded(usage: UsageOutput): boolean {
1010
}
1111

1212
/**
13-
* The org is confirmed on the free tier (not billed for Code usage). False
14-
* when billed OR when the state is unknown — `code_usage_billed` is absent on
15-
* gateways predating the field, and absence must never read as free.
13+
* The org is confirmed on the free tier (not subscribed to Code usage
14+
* billing). False when subscribed OR when the state is unknown —
15+
* `code_usage_subscribed` is absent on gateways predating the field, and
16+
* absence must never read as free.
1617
*/
17-
export function isCodeUsageUnbilled(
18-
usage: Pick<UsageOutput, "code_usage_billed"> | null | undefined,
18+
export function isCodeUsageUnsubscribed(
19+
usage: Pick<UsageOutput, "code_usage_subscribed"> | null | undefined,
1920
): boolean {
20-
return usage?.code_usage_billed === false;
21+
return usage?.code_usage_subscribed === false;
2122
}
2223

2324
export function formatResetTime(

packages/core/src/billing/usageLimitContent.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe("usageBasedLimitContent", () => {
2222
cause: "model_gate",
2323
model: "Claude Opus 4.8",
2424
resetLabel: null,
25-
billed: false,
25+
subscribed: false,
2626
});
2727
expect(content.title).toBe("Unlock premium models");
2828
expect(content.description).toContain("Claude Opus 4.8 isn't");
@@ -34,26 +34,26 @@ describe("usageBasedLimitContent", () => {
3434
cause: "model_gate",
3535
model: null,
3636
resetLabel: null,
37-
billed: undefined,
37+
subscribed: undefined,
3838
});
3939
expect(content.description).toContain("This model isn't");
4040
});
4141

4242
it.each([
43-
// Confirmed-free org: allocation used up, the fix is adding a card.
43+
// Confirmed-unsubscribed org: allocation used up, the fix is adding a card.
4444
[false, "Free usage used up", "Add payment method"],
45-
// Billed org: the fix is raising the spend limit.
45+
// Subscribed org: the fix is raising the spend limit.
4646
[true, "Organization usage limit reached", "Manage billing"],
47-
// Unknown billed state must not read as free.
47+
// Unknown subscription state must not read as free.
4848
[undefined, "Organization usage limit reached", "Manage billing"],
4949
] as const)(
50-
"org_limit with billed=%s -> %s / %s",
51-
(billed, title, actionLabel) => {
50+
"org_limit with subscribed=%s -> %s / %s",
51+
(subscribed, title, actionLabel) => {
5252
const content = usageBasedLimitContent({
5353
cause: "org_limit",
5454
model: null,
5555
resetLabel: null,
56-
billed,
56+
subscribed,
5757
});
5858
expect(content.title).toBe(title);
5959
expect(content.actionLabel).toBe(actionLabel);
@@ -68,7 +68,7 @@ describe("usageBasedLimitContent", () => {
6868
cause,
6969
model: null,
7070
resetLabel: "Resets in 2h",
71-
billed: false,
71+
subscribed: false,
7272
});
7373
expect(content.title).toBe(title);
7474
expect(content.description).toContain("Resets in 2h");

packages/core/src/billing/usageLimitContent.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ export function usageBasedLimitContent(args: {
2727
cause: GatewayLimitCause;
2828
model: string | null;
2929
resetLabel: string | null;
30-
/** usage.code_usage_billed — absent means unknown, not free. */
31-
billed: boolean | undefined;
30+
/** usage.code_usage_subscribed — absent means unknown, not free. */
31+
subscribed: boolean | undefined;
3232
}): UsageLimitContent {
33-
const { cause, model, resetLabel, billed } = args;
33+
const { cause, model, resetLabel, subscribed } = args;
3434

3535
if (cause === "model_gate") {
3636
return {
@@ -42,7 +42,7 @@ export function usageBasedLimitContent(args: {
4242
}
4343

4444
if (cause === "org_limit") {
45-
if (billed === false) {
45+
if (subscribed === false) {
4646
return {
4747
title: "Free usage used up",
4848
description:

packages/core/src/llm-gateway/llm-gateway.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,14 @@ describe("LlmGatewayService.prompt", () => {
202202
expect(retryBody.model).toBe("@cf/zai-org/glm-5.2");
203203
});
204204

205-
it("routes straight to the free-tier model once the org is known unbilled", async () => {
205+
it("routes straight to the free-tier model once the org is known unsubscribed", async () => {
206206
const fetchMock = vi
207207
.fn()
208208
.mockResolvedValueOnce(createJsonResponse(MODEL_GATE_BODY, 403))
209209
.mockImplementation(async () => createJsonResponse(SUCCESS_BODY));
210210
const { service } = createService(fetchMock);
211211

212-
// First call learns "unbilled" from the gate's 403.
212+
// First call learns "unsubscribed" from the gate's 403.
213213
await service.prompt([{ role: "user", content: "hi" }], {
214214
model: "claude-haiku-4-5",
215215
});
@@ -303,22 +303,22 @@ describe("LlmGatewayService.fetchUsage", () => {
303303
createJsonResponse({
304304
...USAGE_BODY,
305305
ai_credits: { exhausted: true },
306-
code_usage_billed: true,
306+
code_usage_subscribed: true,
307307
}),
308308
);
309309
const { service } = createService(fetchMock);
310310

311311
const usage = await service.fetchUsage();
312312

313313
expect(usage.ai_credits?.exhausted).toBe(true);
314-
expect(usage.code_usage_billed).toBe(true);
314+
expect(usage.code_usage_subscribed).toBe(true);
315315
});
316316

317-
it("feeds code_usage_billed into helper model routing", async () => {
317+
it("feeds code_usage_subscribed into helper model routing", async () => {
318318
const fetchMock = vi
319319
.fn()
320320
.mockResolvedValueOnce(
321-
createJsonResponse({ ...USAGE_BODY, code_usage_billed: false }),
321+
createJsonResponse({ ...USAGE_BODY, code_usage_subscribed: false }),
322322
)
323323
.mockResolvedValue(createJsonResponse(SUCCESS_BODY));
324324
const { service } = createService(fetchMock);

packages/core/src/llm-gateway/llm-gateway.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ export class LlmGatewayService {
6060
private readonly endpoints: LlmGatewayEndpoints;
6161
private readonly log: LlmGatewayLogger;
6262

63-
// Last code_usage_billed seen from the gateway (usage fetches ride through
64-
// this service, and the usage monitor refreshes on LLM activity, so this
65-
// stays warm). null until the gateway has told us either way — old gateways
66-
// omit the field entirely.
67-
private lastKnownCodeUsageBilled: boolean | null = null;
63+
// Last code_usage_subscribed seen from the gateway (usage fetches ride
64+
// through this service, and the usage monitor refreshes on LLM activity, so
65+
// this stays warm). null until the gateway has told us either way — old
66+
// gateways omit the field entirely.
67+
private lastKnownCodeUsageSubscribed: boolean | null = null;
6868

6969
async prompt(
7070
messages: LlmMessage[],
@@ -89,7 +89,7 @@ export class LlmGatewayService {
8989
// free tier, route helpers to the free-tier model upfront instead of
9090
// burning a request on the model gate's 403.
9191
const model =
92-
this.lastKnownCodeUsageBilled === false
92+
this.lastKnownCodeUsageSubscribed === false
9393
? FREE_TIER_GATEWAY_MODEL
9494
: requested;
9595
try {
@@ -100,10 +100,10 @@ export class LlmGatewayService {
100100
error.statusCode === 403 &&
101101
classifyGatewayLimitError(error.message) === "model_gate";
102102
if (!isModelGate || model === FREE_TIER_GATEWAY_MODEL) throw error;
103-
// Backstop for a stale billed bit (org just lost billing): the gate
104-
// itself is authoritative that the org isn't billed, so remember that
103+
// Backstop for a stale subscription bit (org just unsubscribed): the gate
104+
// itself is authoritative that the org isn't subscribed, so remember that
105105
// and degrade this call instead of failing it.
106-
this.lastKnownCodeUsageBilled = false;
106+
this.lastKnownCodeUsageSubscribed = false;
107107
this.log.warn("Model gated for free tier, retrying on free-tier model", {
108108
model,
109109
fallbackModel: FREE_TIER_GATEWAY_MODEL,
@@ -282,8 +282,8 @@ export class LlmGatewayService {
282282
}
283283

284284
const usage = usageOutput.parse(await response.json());
285-
if (usage.code_usage_billed !== undefined) {
286-
this.lastKnownCodeUsageBilled = usage.code_usage_billed;
285+
if (usage.code_usage_subscribed !== undefined) {
286+
this.lastKnownCodeUsageSubscribed = usage.code_usage_subscribed;
287287
}
288288
return usage;
289289
}

packages/core/src/usage/schemas.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ export const usageOutput = z.object({
1818
is_rate_limited: z.boolean(),
1919
// Seat-era plan bit; false for everyone once seats are retired.
2020
is_pro: z.boolean(),
21-
// True when the org pays for Code usage (usage-based billing). Absent on
22-
// gateways that predate the field — treat absence as unknown, not false.
23-
code_usage_billed: z.boolean().optional(),
21+
// True when the org is subscribed to Code usage billing (payment method
22+
// on file). Absent on gateways that predate the field — treat absence as
23+
// unknown, not false.
24+
code_usage_subscribed: z.boolean().optional(),
2425
billing_period_end: z.string().datetime().nullable().optional(),
2526
});
2627

packages/ui/src/features/billing/UsageLimitModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function UsageLimitModal() {
3636
const { isPro: seatIsPro } = useSeat();
3737
const usageBillingEnabled = useFeatureFlag(USAGE_BILLING_FLAG);
3838
const cloudRegion = useAuthStateValue((state) => state.cloudRegion);
39-
// Whether the org pays for Code usage — picks the org_limit copy variant.
39+
// The org's Code usage subscription — picks the org_limit copy variant.
4040
const { usage } = useUsage({ enabled: usageBillingEnabled && isOpen });
4141
const isPro = eventIsPro ?? seatIsPro;
4242

@@ -60,7 +60,7 @@ export function UsageLimitModal() {
6060
cause: deriveUsageLimitCause(cause, bucket),
6161
model,
6262
resetLabel,
63-
billed: usage?.code_usage_billed,
63+
subscribed: usage?.code_usage_subscribed,
6464
})
6565
: seatEraLimitContent({ bucket, isPro, resetLabel });
6666

packages/ui/src/features/billing/useFreeUsage.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isCodeUsageUnbilled } from "@posthog/core/billing/usageDisplay";
1+
import { isCodeUsageUnsubscribed } from "@posthog/core/billing/usageDisplay";
22
import type { UsageOutput } from "@posthog/core/usage/schemas";
33
import { USAGE_BILLING_FLAG } from "@posthog/shared";
44
import { useFeatureFlag } from "../feature-flags/useFeatureFlag";
@@ -17,16 +17,16 @@ export function useFreeUsage(billingEnabled: boolean): FreeUsageResult {
1717
const { seat, isPro } = useSeat();
1818
const seatLoaded = seat !== null;
1919
// Seat era: free-seat holders only. Usage era: seats are gone — fetch for
20-
// everyone, then show only orgs the gateway confirms as unbilled (the free
20+
// everyone, then show only orgs the gateway confirms unsubscribed (the free
2121
// tier's per-user allowance is the meaningful meter).
2222
const eligible = usageBillingEnabled
2323
? billingEnabled
2424
: billingEnabled && seatLoaded && !isPro;
2525
const { usage, isLoading } = useUsage({ enabled: eligible });
2626

2727
if (!eligible) return { usage: null, isLoading: false };
28-
if (usageBillingEnabled && !isCodeUsageUnbilled(usage)) {
29-
// Billed org (no per-user caps to meter) or billed state unknown: the
28+
if (usageBillingEnabled && !isCodeUsageUnsubscribed(usage)) {
29+
// Subscribed org (no per-user caps to meter) or unknown state: the
3030
// free-tier bar would be noise or wrong — render nothing.
3131
return { usage: null, isLoading: usage ? false : isLoading };
3232
}

packages/ui/src/features/settings/sections/PlanUsageSettings.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
WarningCircle,
66
} from "@phosphor-icons/react";
77
import {
8-
isCodeUsageUnbilled,
8+
isCodeUsageUnsubscribed,
99
PRO_USAGE_MULTIPLIER,
1010
} from "@posthog/core/billing/usageDisplay";
1111
import type { UsageOutput } from "@posthog/core/usage/schemas";
@@ -528,9 +528,9 @@ function UsageBasedPlanUsage({
528528
billingUrl,
529529
onOpenBilling,
530530
}: UsageBasedPlanUsageProps) {
531-
// Tri-state: unknown (absent field) must render as billed, never as free.
532-
const unbilled = isCodeUsageUnbilled(usage);
533-
const billed = usage?.code_usage_billed === true;
531+
// Tri-state: unknown (absent field) must render as subscribed, never free.
532+
const unsubscribed = isCodeUsageUnsubscribed(usage);
533+
const subscribed = usage?.code_usage_subscribed === true;
534534
const orgLimitReached = usage?.ai_credits?.exhausted === true;
535535

536536
return (
@@ -571,28 +571,30 @@ function UsageBasedPlanUsage({
571571
<Flex align="center" justify="between">
572572
<Flex direction="column" gap="1">
573573
<Text className="font-bold text-base">
574-
{unbilled ? "Free tier" : "Usage-based billing"}
574+
{unsubscribed ? "Free tier" : "Usage-based billing"}
575575
</Text>
576576
<Text className="text-(--gray-11) text-sm">
577-
{unbilled
577+
{unsubscribed
578578
? "Your organization's first $20 of usage each month is included, with access to open models. Add a payment method to unlock premium models — you only pay for what you use."
579-
: "Your organization pays for PostHog Code usage at cost — no seats, no subscriptions. The first $20 each month is included."}
579+
: "Your organization pays for PostHog Code usage at cost — no seats, no per-user plans. The first $20 each month is included."}
580580
</Text>
581581
</Flex>
582-
{billed && (
582+
{subscribed && (
583583
<Badge variant="soft" color="green" radius="full">
584584
Active
585585
</Badge>
586586
)}
587587
</Flex>
588588
<Button
589589
size="1"
590-
variant={unbilled ? "solid" : "outline"}
590+
variant={unsubscribed ? "solid" : "outline"}
591591
disabled={!billingUrl}
592592
onClick={onOpenBilling}
593593
className="self-start"
594594
>
595-
{unbilled ? "Add payment method" : "Manage billing and spend limits"}
595+
{unsubscribed
596+
? "Add payment method"
597+
: "Manage billing and spend limits"}
596598
<ArrowSquareOut size={12} />
597599
</Button>
598600
</Flex>
@@ -608,7 +610,7 @@ function UsageBasedPlanUsage({
608610
>
609611
<Spinner size="2" />
610612
</Flex>
611-
) : unbilled && usage ? (
613+
) : unsubscribed && usage ? (
612614
<Flex direction="column" gap="3">
613615
<UsageMeter
614616
label="Monthly free usage"

0 commit comments

Comments
 (0)