Skip to content

Commit d161b47

Browse files
committed
Skip billing for missing customers/orgs
1 parent 7c4d69b commit d161b47

File tree

2 files changed

+90
-71
lines changed

2 files changed

+90
-71
lines changed

apps/api/src/services/billing.live.ts

Lines changed: 87 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@ import { loadEnv } from '../env.js'
66
export function createAutumnBillingApi(secretKey: string): BillingApi {
77
const autumn = new Autumn({ secretKey })
88

9+
const allowByDefault = (featureId: string) =>
10+
Effect.succeed({
11+
allowed: true,
12+
featureId,
13+
balance: null,
14+
unlimited: undefined,
15+
} as const)
16+
917
return {
1018
check: (customerId, featureId) =>
11-
Effect.tryPromise(() =>
12-
autumn.check({ customer_id: customerId, feature_id: featureId }),
13-
).pipe(
19+
!customerId
20+
? allowByDefault(featureId)
21+
: Effect.tryPromise(() =>
22+
autumn.check({ customer_id: customerId, feature_id: featureId }),
23+
).pipe(
1424
Effect.map((result) => ({
1525
allowed: result.data?.allowed ?? true,
1626
featureId,
@@ -31,79 +41,85 @@ export function createAutumnBillingApi(secretKey: string): BillingApi {
3141
),
3242

3343
track: (customerId, featureId, value) =>
34-
Effect.tryPromise(() =>
35-
autumn.track({
36-
customer_id: customerId,
37-
feature_id: featureId,
38-
value: value ?? 1,
39-
}),
40-
).pipe(
41-
Effect.tapError((err) =>
42-
Effect.logWarning(`Billing track failed for ${customerId}/${featureId}: ${err}`),
43-
),
44-
Effect.catchAll(() => Effect.void),
45-
Effect.map(() => undefined),
46-
),
44+
!customerId
45+
? Effect.void
46+
: Effect.tryPromise(() =>
47+
autumn.track({
48+
customer_id: customerId,
49+
feature_id: featureId,
50+
value: value ?? 1,
51+
}),
52+
).pipe(
53+
Effect.tapError((err) =>
54+
Effect.logWarning(`Billing track failed for ${customerId}/${featureId}: ${err}`),
55+
),
56+
Effect.catchAll(() => Effect.void),
57+
Effect.map(() => undefined),
58+
),
4759

4860
trackCompute: (customerId, dollarAmount, sandboxId) =>
49-
Effect.tryPromise(() =>
50-
autumn.track({
51-
customer_id: customerId,
52-
feature_id: 'compute',
53-
value: dollarAmount,
54-
properties: { sandbox_id: sandboxId },
55-
}),
56-
).pipe(
57-
Effect.tapError((err) =>
58-
Effect.logWarning(`Billing trackCompute failed for ${customerId}/${sandboxId}: ${err}`),
59-
),
60-
Effect.catchAll(() => Effect.void),
61-
Effect.map(() => undefined),
62-
),
61+
!customerId
62+
? Effect.void
63+
: Effect.tryPromise(() =>
64+
autumn.track({
65+
customer_id: customerId,
66+
feature_id: 'compute',
67+
value: dollarAmount,
68+
properties: { sandbox_id: sandboxId },
69+
}),
70+
).pipe(
71+
Effect.tapError((err) =>
72+
Effect.logWarning(`Billing trackCompute failed for ${customerId}/${sandboxId}: ${err}`),
73+
),
74+
Effect.catchAll(() => Effect.void),
75+
Effect.map(() => undefined),
76+
),
6377

6478
checkCredits: (customerId, estimatedDollars) =>
65-
Effect.tryPromise(() =>
66-
autumn.check({
67-
customer_id: customerId,
68-
feature_id: 'credits',
69-
// Always send a required_balance so Autumn checks actual balance.
70-
// Use the estimate if provided, otherwise check for at least $0.01.
71-
required_balance: estimatedDollars > 0 ? estimatedDollars : 0.01,
72-
}),
73-
).pipe(
74-
Effect.map((result) => ({
75-
allowed: result.data?.allowed ?? true,
76-
featureId: 'credits',
77-
balance: result.data?.balance,
78-
unlimited: result.data?.unlimited,
79-
})),
80-
Effect.tapError((err) =>
81-
Effect.logWarning(`Failed to check credits for ${customerId}: ${err}`),
82-
),
83-
Effect.catchAll(() =>
84-
Effect.succeed({
85-
allowed: true,
86-
featureId: 'credits',
87-
balance: null,
88-
unlimited: undefined,
89-
}),
90-
),
91-
),
79+
!customerId
80+
? allowByDefault('credits')
81+
: Effect.tryPromise(() =>
82+
autumn.check({
83+
customer_id: customerId,
84+
feature_id: 'credits',
85+
required_balance: estimatedDollars > 0 ? estimatedDollars : 0.01,
86+
}),
87+
).pipe(
88+
Effect.map((result) => ({
89+
allowed: result.data?.allowed ?? true,
90+
featureId: 'credits',
91+
balance: result.data?.balance,
92+
unlimited: result.data?.unlimited,
93+
})),
94+
Effect.tapError((err) =>
95+
Effect.logWarning(`Failed to check credits for ${customerId}: ${err}`),
96+
),
97+
Effect.catchAll(() =>
98+
Effect.succeed({
99+
allowed: true,
100+
featureId: 'credits',
101+
balance: null,
102+
unlimited: undefined,
103+
}),
104+
),
105+
),
92106
getBillingTier: (customerId) =>
93-
Effect.tryPromise(async () => {
94-
const customer = await autumn.customers.get(customerId)
95-
const products = customer?.data?.products ?? []
96-
const hasMax = products.some(
97-
(p: { id?: string; status?: string }) =>
98-
p.id === 'max' && p.status === 'active',
99-
)
100-
return hasMax ? 'max' as const : 'free' as const
101-
}).pipe(
102-
Effect.tapError((err) =>
103-
Effect.logWarning(`Failed to get billing tier for ${customerId}: ${err}`),
104-
),
105-
Effect.catchAll(() => Effect.succeed('free' as const)),
106-
),
107+
!customerId
108+
? Effect.succeed('free' as const)
109+
: Effect.tryPromise(async () => {
110+
const customer = await autumn.customers.get(customerId)
111+
const products = customer?.data?.products ?? []
112+
const hasMax = products.some(
113+
(p: { id?: string; status?: string }) =>
114+
p.id === 'max' && p.status === 'active',
115+
)
116+
return hasMax ? 'max' as const : 'free' as const
117+
}).pipe(
118+
Effect.tapError((err) =>
119+
Effect.logWarning(`Failed to get billing tier for ${customerId}: ${err}`),
120+
),
121+
Effect.catchAll(() => Effect.succeed('free' as const)),
122+
),
107123
}
108124
}
109125

apps/api/src/workers/credit-metering.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export function meterSandbox(
2323
// Skip sandboxes that never started — no compute to bill
2424
if (!sandbox.startedAt) return
2525

26+
// Skip sandboxes with no org — nothing to bill
27+
if (!sandbox.orgId) return
28+
2629
const billing = yield* BillingService
2730
const repo = yield* SandboxRepo
2831

0 commit comments

Comments
 (0)