Skip to content

Commit eb7bb83

Browse files
committed
refactor(balances): limit_reached cap selection in its own files
1 parent bdedab4 commit eb7bb83

4 files changed

Lines changed: 103 additions & 91 deletions

File tree

server/src/internal/balances/trackWebhooks/checkLimitReached.ts

Lines changed: 2 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -6,101 +6,12 @@ import {
66
type FullCustomer,
77
type FullSubject,
88
fullCustomerToTags,
9-
fullSubjectToUsageWindowLimits,
10-
getCurrentUsageWindowUsage,
11-
orgToInStatuses,
12-
type UsageLimitFilter,
13-
type UsageLimitWebhookBlock,
14-
usageLimitFilterMatchesProperties,
159
WebhookEventType,
1610
} from "@autumn/shared";
1711
import { sendSvixEvent } from "@/external/svix/svixHelpers.js";
1812
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
19-
import { usageWindowLimitToWebhookBlock } from "@/internal/balances/utils/usageWindows/usageWindowLimitToWebhookBlock.js";
20-
21-
type BlockingUsageLimit = {
22-
block: UsageLimitWebhookBlock;
23-
filter: UsageLimitFilter | undefined;
24-
};
25-
26-
/**
27-
* The cap that blocked this event. Enforcement stops at the cap with the least
28-
* headroom, so the webhook reports that one, filter included, from one source.
29-
*/
30-
const findBlockingUsageLimit = ({
31-
ctx,
32-
fullSubject,
33-
feature,
34-
eventProperties,
35-
now,
36-
}: {
37-
ctx: AutumnContext;
38-
fullSubject: FullSubject;
39-
feature: Feature;
40-
eventProperties?: Record<string, unknown> | null;
41-
now: number;
42-
}): BlockingUsageLimit | undefined => {
43-
const usageWindows = fullSubject.usage_windows ?? [];
44-
const measured = fullSubjectToUsageWindowLimits({
45-
fullSubject,
46-
featureIds: [feature.id],
47-
features: ctx.features,
48-
now,
49-
inStatuses: orgToInStatuses({ org: ctx.org }),
50-
})
51-
.filter((limit) =>
52-
usageLimitFilterMatchesProperties({
53-
filterProperties: limit.filter_properties,
54-
eventProperties,
55-
}),
56-
)
57-
.map((limit) => ({
58-
limit,
59-
usage: getCurrentUsageWindowUsage({ usageWindows, limit, now }),
60-
}))
61-
.sort(
62-
(left, right) =>
63-
left.limit.limit - left.usage - (right.limit.limit - right.usage),
64-
);
65-
66-
const tightest = measured[0];
67-
if (!tightest || tightest.usage < tightest.limit.limit) return undefined;
68-
69-
const block = usageWindowLimitToWebhookBlock({
70-
limit: tightest.limit,
71-
usage: tightest.usage,
72-
});
73-
if (!block) return undefined;
74-
75-
return {
76-
block,
77-
filter: tightest.limit.filter_properties
78-
? { properties: tightest.limit.filter_properties }
79-
: undefined,
80-
};
81-
};
82-
83-
/** Legacy deductions carry no FullSubject; read the exhausted filtered cap off the evaluated subject. */
84-
const findBlockedFilterOnSubject = ({
85-
subject,
86-
feature,
87-
eventProperties,
88-
}: {
89-
subject: ApiCustomerV5 | ApiEntityV2;
90-
feature: Feature;
91-
eventProperties?: Record<string, unknown> | null;
92-
}): UsageLimitFilter | undefined =>
93-
subject.billing_controls?.usage_limits?.find(
94-
(usageLimit) =>
95-
usageLimit.feature_id === feature.id &&
96-
usageLimit.enabled !== false &&
97-
usageLimit.filter != null &&
98-
usageLimitFilterMatchesProperties({
99-
filterProperties: usageLimit.filter.properties,
100-
eventProperties,
101-
}) &&
102-
(usageLimit.usage ?? 0) >= usageLimit.limit,
103-
)?.filter;
13+
import { findBlockedFilterOnSubject } from "./limitReached/findBlockedFilterOnSubject.js";
14+
import { findBlockingUsageLimit } from "./limitReached/findBlockingUsageLimit.js";
10415

10516
// Subjects must be built via buildEvaluationSubject, or plan-level / percentage
10617
// caps are invisible here and the allowed -> blocked transition never fires.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
type ApiCustomerV5,
3+
type ApiEntityV2,
4+
type Feature,
5+
type UsageLimitFilter,
6+
usageLimitFilterMatchesProperties,
7+
} from "@autumn/shared";
8+
9+
/** Legacy deductions carry no FullSubject; read the exhausted filtered cap off the evaluated subject. */
10+
export const findBlockedFilterOnSubject = ({
11+
subject,
12+
feature,
13+
eventProperties,
14+
}: {
15+
subject: ApiCustomerV5 | ApiEntityV2;
16+
feature: Feature;
17+
eventProperties?: Record<string, unknown> | null;
18+
}): UsageLimitFilter | undefined =>
19+
subject.billing_controls?.usage_limits?.find(
20+
(usageLimit) =>
21+
usageLimit.feature_id === feature.id &&
22+
usageLimit.enabled !== false &&
23+
usageLimit.filter != null &&
24+
usageLimitFilterMatchesProperties({
25+
filterProperties: usageLimit.filter.properties,
26+
eventProperties,
27+
}) &&
28+
(usageLimit.usage ?? 0) >= usageLimit.limit,
29+
)?.filter;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
type Feature,
3+
type FullSubject,
4+
fullSubjectToUsageWindowLimits,
5+
getCurrentUsageWindowUsage,
6+
orgToInStatuses,
7+
usageLimitFilterMatchesProperties,
8+
} from "@autumn/shared";
9+
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
10+
import { usageWindowLimitToWebhookBlock } from "@/internal/balances/utils/usageWindows/usageWindowLimitToWebhookBlock.js";
11+
import type { BlockingUsageLimit } from "./types/blockingUsageLimit.js";
12+
13+
/**
14+
* The cap that blocked this event. Enforcement stops at the cap with the least
15+
* headroom, so the webhook reports that one, filter included, from one source.
16+
*/
17+
export const findBlockingUsageLimit = ({
18+
ctx,
19+
fullSubject,
20+
feature,
21+
eventProperties,
22+
now,
23+
}: {
24+
ctx: AutumnContext;
25+
fullSubject: FullSubject;
26+
feature: Feature;
27+
eventProperties?: Record<string, unknown> | null;
28+
now: number;
29+
}): BlockingUsageLimit | undefined => {
30+
const usageWindows = fullSubject.usage_windows ?? [];
31+
const measured = fullSubjectToUsageWindowLimits({
32+
fullSubject,
33+
featureIds: [feature.id],
34+
features: ctx.features,
35+
now,
36+
inStatuses: orgToInStatuses({ org: ctx.org }),
37+
})
38+
.filter((limit) =>
39+
usageLimitFilterMatchesProperties({
40+
filterProperties: limit.filter_properties,
41+
eventProperties,
42+
}),
43+
)
44+
.map((limit) => ({
45+
limit,
46+
headroom:
47+
limit.limit - getCurrentUsageWindowUsage({ usageWindows, limit, now }),
48+
}))
49+
.sort((left, right) => left.headroom - right.headroom);
50+
51+
const tightest = measured[0];
52+
if (!tightest || tightest.headroom > 0) return undefined;
53+
54+
const block = usageWindowLimitToWebhookBlock({
55+
limit: tightest.limit,
56+
usage: tightest.limit.limit - tightest.headroom,
57+
});
58+
if (!block) return undefined;
59+
60+
return {
61+
block,
62+
filter: tightest.limit.filter_properties
63+
? { properties: tightest.limit.filter_properties }
64+
: undefined,
65+
};
66+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { UsageLimitFilter, UsageLimitWebhookBlock } from "@autumn/shared";
2+
3+
export type BlockingUsageLimit = {
4+
block: UsageLimitWebhookBlock;
5+
filter: UsageLimitFilter | undefined;
6+
};

0 commit comments

Comments
 (0)