feat(scheduler): per-group profit control for token account admission

Group pricing (rate multiplier, peak windows, per-user overrides) and
account cost (accounts.rate_multiplier) already live side by side, but
nothing stops the scheduler from handing a request to an account whose
cost multiplier exceeds what the group's pricing can profitably serve.
Add an opt-in per-group profit gate that filters scheduling candidates
by a margin rule, while ordering, scoring, stickiness and breakers keep
working unchanged among qualified accounts.

Admission rule: an account qualifies iff U <= D * (1 - min_margin -
safety_buffer) within a small relative epsilon, where U is
accounts.rate_multiplier (0 is legal; missing/negative/NaN/Inf are
conservatively rejected as invalid) and D is the requester's effective
downstream multiplier (user-group override ?? group default, times the
group peak factor) frozen at the request's pricing instant.

- groups gain profit_control_enabled / profit_min_margin /
  profit_safety_buffer (migration 191); the durable auth-cache
  invalidation trigger additionally watches the profit and pricing
  columns (migration 192) so out-of-band group edits cannot leave
  stale auth snapshots; GetByKeyForAuth explicitly projects the new
  columns and the API-key auth snapshot version is bumped to force a
  refresh of pre-existing snapshots
- request-level pricing instant: token entry points install pricingAt
  into ctx; the profit threshold D and the RecordUsage peak factor
  read the same instant, so one request never changes price mid-flight
  across waits/retries/failover (media and unwired paths keep the
  existing record-time semantics)
- the gate covers token requests on openai, anthropic, gemini, grok
  and antigravity groups: OpenAI-family handlers via
  WithOpenAIRequestPricingContext (responses incl. WS bridge, chat
  completions, messages, embeddings, alpha search), the shared gateway
  via WithGatewayTokenRequestPricing (messages, chat completions,
  responses, gemini model actions); composite groups cannot enable it
  directly; image/video/models/usage/count_tokens stay ungated and an
  explicit image-generation intent suppresses the gate end to end
- post-slot recheck: after a slot is acquired the account is re-read
  via SchedulerSnapshotService.GetAccount (scheduler cache, then DB;
  only when both fail the check fails open with WARN + metric); a
  vetoed account releases its slot and joins the request's exclusion
  set for reselection; sticky bindings are written only after the
  final check passes, and an over-threshold sticky account is skipped,
  not deleted, so it comes back once its rate recovers
- sticky-session cache contract: GatewayCache.GetSessionAccountID now
  returns ErrStickySessionNotFound on a miss (mapped from redis.Nil in
  the repository implementation, mirroring ErrRefreshTokenNotFound) so
  the profit sticky path can distinguish "no binding yet" from a real
  read failure without importing the cache driver in service code
- cross-group re-entry (composite parent -> member group) resolves the
  gate against the member group and clears a stale parent gate instead
  of letting a foreign threshold veto accounts
- per-platform/group activity counters (installs, threshold vetoes,
  invalid-rate vetoes, refresh failures) for observability
- admin UI: profit-control section on the five platforms' group forms
  with percent input, validation and platform-switch reset; group
  create/update/duplicate normalize and validate the config at a
  single choke point
- cmd/profit-preview: offline what-if tool that replays the production
  admission semantics over an exported config/account/override/model
  dump, reports per-model admitted-account counts under the default
  and the worst-case (lowest user override) D, and surfaces probe-sync
  staleness as warnings without affecting admission

Tests: service unit coverage for gate resolution/veto/threshold
epsilon/pricing instant/suppress marker/scheduler filtering and
post-slot recheck (incl. -race on the profit surface), unit-tagged
handler slot-recheck and capability-mapping regressions, sqlmock and
real-PostgreSQL integration regressions for the GetByKeyForAuth
projection and the migration-192 trigger watch list, API contract
update, and frontend specs for the five-platform form helpers.
This commit is contained in:
Brisbanehuang
2026-08-01 22:39:31 +08:00
committed by shaw
parent 0b6b4ea956
commit 20ad5ec506
68 changed files with 4752 additions and 100 deletions
@@ -1006,6 +1006,18 @@ export default {
peakMultiplier: 'Peak multiplier',
multiplierHint: 'Applies to token billing multiplier; image tokens in token billing are also affected. 0 means peak token requests are billed at 0x.'
},
profitControl: {
enable: 'Enable profit control',
enabledHint: 'Scheduling only admits accounts whose account multiplier ≤ the request\'s effective downstream multiplier × (1 − min margin − safety buffer). Account multipliers may be maintained manually or synchronized from probes; existing ordering, stickiness and breakers keep working among qualified accounts. Image/video scheduling is not covered yet.',
disabledHint: 'When disabled, scheduling does no profit filtering: accounts whose account multiplier exceeds the downstream multiplier can still be selected, which may produce loss-making requests.',
minMargin: 'Min gross margin (%)',
minMarginHint: 'Percent input, e.g. 30 means 30%; stored as a decimal on the backend',
safetyBuffer: 'Safety buffer (%)',
safetyBufferHint: 'Added to min margin and deducted from the downstream multiplier; defaults to 0',
marginRangeError: 'Min gross margin must be between 0 and 100 (exclusive)',
bufferRangeError: 'Safety buffer must be between 0 and 100 (exclusive)',
sumTooHigh: 'Min gross margin plus safety buffer must be less than 100%, otherwise every account would be excluded'
},
modelsList: {
title: 'Custom /v1/models Model List',
hint: 'Only changes the /v1/models response. Whitelist model calls and account routing are unchanged.',
@@ -1003,6 +1003,18 @@ export default {
peakMultiplier: '高峰倍率',
multiplierHint: '作用于 token 计费倍率;token 计费的图片 token 同样适用,0 表示高峰 token 请求按 0 倍计费'
},
profitControl: {
enable: '启用利润控制',
enabledHint: '调度时仅允许"账号倍率 ≤ 请求实际下游倍率 ×(1 − 最低毛利率 − 安全缓冲)"的账号进入候选池;账号倍率可手工维护或由探测同步,既有排序、粘性与熔断在合格账号间照常工作。图片/视频调度暂不参与。',
disabledHint: '关闭后调度不做利润过滤,账号倍率高于下游倍率的账号也会被选中,可能产生亏损请求。',
minMargin: '最低毛利率(%)',
minMarginHint: '百分比输入,如 30 表示 30%;后端按小数存储',
safetyBuffer: '安全缓冲(%)',
safetyBufferHint: '与最低毛利率相加后从下游倍率中扣除,默认 0',
marginRangeError: '最低毛利率应在 0 到 100 之间(不含 100)',
bufferRangeError: '安全缓冲应在 0 到 100 之间(不含 100)',
sumTooHigh: '最低毛利率与安全缓冲之和必须小于 100%,否则将排除全部账号'
},
modelsList: {
title: '自定义 /v1/models 模型列表',
hint: '仅影响 /v1/models 展示结果,不影响白名单模型调用和账号调度。',
+12
View File
@@ -549,6 +549,10 @@ export interface Group {
peak_start: string
peak_end: string
peak_rate_multiplier: number
// 分组利润控制(openai/anthropic/gemini/grok/antigravity 分组可启用;margin/buffer 为小数存储)
profit_control_enabled: boolean
profit_min_margin: number
profit_safety_buffer: number
// Claude Code 客户端限制
claude_code_only: boolean
fallback_group_id: number | null
@@ -741,6 +745,10 @@ export interface CreateGroupRequest {
peak_start?: string
peak_end?: string
peak_rate_multiplier?: number
// 分组利润控制(五个 token 平台;margin/buffer 为小数)
profit_control_enabled?: boolean
profit_min_margin?: number
profit_safety_buffer?: number
claude_code_only?: boolean
fallback_group_id?: number | null
fallback_group_id_on_invalid_request?: number | null
@@ -792,6 +800,10 @@ export interface UpdateGroupRequest {
peak_start?: string
peak_end?: string
peak_rate_multiplier?: number
// 分组利润控制(五个 token 平台;margin/buffer 为小数)
profit_control_enabled?: boolean
profit_min_margin?: number
profit_safety_buffer?: number
claude_code_only?: boolean
fallback_group_id?: number | null
fallback_group_id_on_invalid_request?: number | null
+177
View File
@@ -1152,6 +1152,56 @@
</div>
</div>
<!-- 分组利润控制(五个平台 token 请求) -->
<div v-if="isProfitControlPlatform(createForm.platform)" class="border-t pt-4">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="createForm.profit_control_enabled"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span>{{ t("admin.groups.profitControl.enable") }}</span>
</label>
<p class="mb-3 mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{
createForm.profit_control_enabled
? t("admin.groups.profitControl.enabledHint")
: t("admin.groups.profitControl.disabledHint")
}}
</p>
<div
v-if="createForm.profit_control_enabled"
class="mb-3 grid grid-cols-1 gap-3 sm:grid-cols-2"
>
<div>
<label class="input-label">{{ t("admin.groups.profitControl.minMargin") }}</label>
<input
v-model.number="createForm.profit_min_margin_percent"
type="number"
step="0.1"
min="0"
max="99.99"
class="input"
placeholder="0"
:title="t('admin.groups.profitControl.minMarginHint')"
/>
</div>
<div>
<label class="input-label">{{ t("admin.groups.profitControl.safetyBuffer") }}</label>
<input
v-model.number="createForm.profit_safety_buffer_percent"
type="number"
step="0.1"
min="0"
max="99.99"
class="input"
placeholder="0"
:title="t('admin.groups.profitControl.safetyBufferHint')"
/>
</div>
</div>
</div>
<!-- 支持的模型系列(仅 antigravity 平台) -->
<div v-if="createForm.platform === 'antigravity'" class="border-t pt-4">
<div class="mb-1.5 flex items-center gap-1">
@@ -2707,6 +2757,56 @@
</div>
</div>
<!-- 分组利润控制(五个平台 token 请求) -->
<div v-if="isProfitControlPlatform(editForm.platform)" class="border-t pt-4">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="editForm.profit_control_enabled"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span>{{ t("admin.groups.profitControl.enable") }}</span>
</label>
<p class="mb-3 mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{
editForm.profit_control_enabled
? t("admin.groups.profitControl.enabledHint")
: t("admin.groups.profitControl.disabledHint")
}}
</p>
<div
v-if="editForm.profit_control_enabled"
class="mb-3 grid grid-cols-1 gap-3 sm:grid-cols-2"
>
<div>
<label class="input-label">{{ t("admin.groups.profitControl.minMargin") }}</label>
<input
v-model.number="editForm.profit_min_margin_percent"
type="number"
step="0.1"
min="0"
max="99.99"
class="input"
placeholder="0"
:title="t('admin.groups.profitControl.minMarginHint')"
/>
</div>
<div>
<label class="input-label">{{ t("admin.groups.profitControl.safetyBuffer") }}</label>
<input
v-model.number="editForm.profit_safety_buffer_percent"
type="number"
step="0.1"
min="0"
max="99.99"
class="input"
placeholder="0"
:title="t('admin.groups.profitControl.safetyBufferHint')"
/>
</div>
</div>
</div>
<!-- 支持的模型系列(仅 antigravity 平台) -->
<div v-if="editForm.platform === 'antigravity'" class="border-t pt-4">
<div class="mb-1.5 flex items-center gap-1">
@@ -4095,6 +4195,13 @@ import {
} from "./groupsModelsList";
import { createModelsListCandidatesTracker } from "./groupsModelsListCandidates";
import { normalizeSupportedModelScopesForPlatform } from "./groupsSupportedModelScopes";
import {
isProfitControlPlatform,
profitPercentToDecimal,
profitDecimalToPercent,
validateProfitControlFormState,
type ProfitControlFormState,
} from "./groupsProfitControl";
import {
normalizeReasoningEffortForPlatform,
reasoningEffortMappingsToAPI,
@@ -4618,6 +4725,10 @@ const createForm = reactive({
peak_start: "",
peak_end: "",
peak_rate_multiplier: 1.0,
// 分组利润控制(五个 token 平台);界面按百分比输入,提交时转小数
profit_control_enabled: false,
profit_min_margin_percent: 0,
profit_safety_buffer_percent: 0,
// Claude Code 客户端限制(仅 anthropic 平台使用)
claude_code_only: false,
fallback_group_id: null as number | null,
@@ -4968,6 +5079,10 @@ const editForm = reactive({
peak_start: "",
peak_end: "",
peak_rate_multiplier: 1.0,
// 分组利润控制(五个 token 平台);界面按百分比输入,提交时转小数
profit_control_enabled: false,
profit_min_margin_percent: 0,
profit_safety_buffer_percent: 0,
// Claude Code 客户端限制(仅 anthropic 平台使用)
claude_code_only: false,
fallback_group_id: null as number | null,
@@ -5412,6 +5527,9 @@ const closeCreateModal = () => {
createForm.peak_start = "";
createForm.peak_end = "";
createForm.peak_rate_multiplier = 1.0;
createForm.profit_control_enabled = false;
createForm.profit_min_margin_percent = 0;
createForm.profit_safety_buffer_percent = 0;
createForm.claude_code_only = false;
createForm.fallback_group_id = null;
createForm.fallback_group_id_on_invalid_request = null;
@@ -5459,6 +5577,19 @@ const normalizeRateMultiplier = (
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 1;
};
// 利润控制表单辅助(换算与校验逻辑见 groupsProfitControl.ts,便于单测)。
const percentToDecimal = profitPercentToDecimal;
const decimalToPercent = profitDecimalToPercent;
const validateProfitControlForm = (form: ProfitControlFormState): boolean => {
const errorKey = validateProfitControlFormState(form);
if (errorKey) {
appStore.showError(t(`admin.groups.profitControl.${errorKey}`));
return false;
}
return true;
};
const handleCreateGroup = async () => {
if (!createForm.name.trim()) {
appStore.showError(t("admin.groups.nameRequired"));
@@ -5471,6 +5602,9 @@ const handleCreateGroup = async () => {
) {
return;
}
if (!validateProfitControlForm(createForm)) {
return;
}
submitting.value = true;
try {
// 构建请求数据,包含模型路由配置
@@ -5506,7 +5640,17 @@ const handleCreateGroup = async () => {
reasoning_effort_mappings: reasoningEffortMappingsToAPI(
createForm.reasoning_effort_mappings,
),
// 利润控制:界面百分比转小数提交;仅五个 token 平台可启用
profit_control_enabled:
isProfitControlPlatform(createForm.platform) &&
createForm.profit_control_enabled,
profit_min_margin: percentToDecimal(createForm.profit_min_margin_percent),
profit_safety_buffer: percentToDecimal(
createForm.profit_safety_buffer_percent,
),
};
delete (requestData as Record<string, unknown>).profit_min_margin_percent;
delete (requestData as Record<string, unknown>).profit_safety_buffer_percent;
// v-model.number 清空输入框时产生 "",转为 null 让后端设为无限制
const emptyToNull = (v: any) => (v === "" ? null : v);
requestData.daily_limit_usd = emptyToNull(requestData.daily_limit_usd);
@@ -5594,6 +5738,13 @@ const handleEdit = async (group: AdminGroup) => {
editForm.peak_start = group.peak_start ?? "";
editForm.peak_end = group.peak_end ?? "";
editForm.peak_rate_multiplier = group.peak_rate_multiplier ?? 1.0;
editForm.profit_control_enabled = group.profit_control_enabled ?? false;
editForm.profit_min_margin_percent = decimalToPercent(
group.profit_min_margin ?? 0,
);
editForm.profit_safety_buffer_percent = decimalToPercent(
group.profit_safety_buffer ?? 0,
);
editForm.claude_code_only = group.claude_code_only || false;
editForm.fallback_group_id = group.fallback_group_id;
editForm.fallback_group_id_on_invalid_request =
@@ -5654,6 +5805,9 @@ const closeEditModal = () => {
editForm.peak_start = "";
editForm.peak_end = "";
editForm.peak_rate_multiplier = 1.0;
editForm.profit_control_enabled = false;
editForm.profit_min_margin_percent = 0;
editForm.profit_safety_buffer_percent = 0;
editForm.video_rate_independent = false;
editForm.video_rate_multiplier = 1;
editForm.video_price_480p = null;
@@ -5678,6 +5832,9 @@ const handleUpdateGroup = async () => {
) {
return;
}
if (!validateProfitControlForm(editForm)) {
return;
}
submitting.value = true;
try {
@@ -5720,7 +5877,17 @@ const handleUpdateGroup = async () => {
reasoning_effort_mappings: reasoningEffortMappingsToAPI(
editForm.reasoning_effort_mappings,
),
// 利润控制:界面百分比转小数提交;仅五个 token 平台可启用
profit_control_enabled:
isProfitControlPlatform(editForm.platform) &&
editForm.profit_control_enabled,
profit_min_margin: percentToDecimal(editForm.profit_min_margin_percent),
profit_safety_buffer: percentToDecimal(
editForm.profit_safety_buffer_percent,
),
};
delete (payload as Record<string, unknown>).profit_min_margin_percent;
delete (payload as Record<string, unknown>).profit_safety_buffer_percent;
// v-model.number 清空输入框时产生 "",转为 null 让后端设为无限制
const emptyToNull = (v: any) => (v === "" ? null : v);
payload.daily_limit_usd = emptyToNull(payload.daily_limit_usd);
@@ -6068,6 +6235,11 @@ watch(
resetMessagesDispatchFormState(createForm);
createForm.allow_live = false;
}
if (!isProfitControlPlatform(newVal)) {
createForm.profit_control_enabled = false;
createForm.profit_min_margin_percent = 0;
createForm.profit_safety_buffer_percent = 0;
}
createForm.max_reasoning_effort = normalizeReasoningEffortForPlatform(
newVal,
createForm.max_reasoning_effort,
@@ -6111,6 +6283,11 @@ watch(
resetMessagesDispatchFormState(editForm);
editForm.allow_live = false;
}
if (!isProfitControlPlatform(newVal)) {
editForm.profit_control_enabled = false;
editForm.profit_min_margin_percent = 0;
editForm.profit_safety_buffer_percent = 0;
}
editForm.max_reasoning_effort = normalizeReasoningEffortForPlatform(
newVal,
editForm.max_reasoning_effort,
@@ -0,0 +1,153 @@
import { describe, expect, it } from "vitest";
import {
profitDecimalToPercent,
profitPercentToDecimal,
validateProfitControlFormState,
type ProfitControlFormState,
} from "../groupsProfitControl";
const formState = (
overrides: Partial<ProfitControlFormState> = {},
): ProfitControlFormState => ({
platform: "openai",
profit_control_enabled: true,
profit_min_margin_percent: 30,
profit_safety_buffer_percent: 0,
...overrides,
});
describe("profitPercentToDecimal", () => {
it("converts percent input to backend decimal", () => {
expect(profitPercentToDecimal(30)).toBe(0.3);
expect(profitPercentToDecimal(5)).toBe(0.05);
expect(profitPercentToDecimal(33.33)).toBe(0.3333);
expect(profitPercentToDecimal(99.99)).toBe(0.9999);
});
it("rounds to four decimal places matching decimal(10,4) storage", () => {
expect(profitPercentToDecimal(33.333)).toBe(0.3333);
expect(profitPercentToDecimal(0.005)).toBe(0.0001);
});
it("treats empty, invalid and non-positive input as zero", () => {
expect(profitPercentToDecimal("")).toBe(0);
expect(profitPercentToDecimal(null)).toBe(0);
expect(profitPercentToDecimal(undefined)).toBe(0);
expect(profitPercentToDecimal("abc")).toBe(0);
expect(profitPercentToDecimal(-5)).toBe(0);
expect(profitPercentToDecimal(0)).toBe(0);
});
});
describe("profitDecimalToPercent", () => {
it("converts backend decimal to percent without float tail noise", () => {
expect(profitDecimalToPercent(0.3)).toBe(30);
expect(profitDecimalToPercent(0.05)).toBe(5);
expect(profitDecimalToPercent(0.3333)).toBe(33.33);
expect(profitDecimalToPercent(0.9999)).toBe(99.99);
});
it("treats missing and non-positive values as zero", () => {
expect(profitDecimalToPercent(null)).toBe(0);
expect(profitDecimalToPercent(undefined)).toBe(0);
expect(profitDecimalToPercent(0)).toBe(0);
expect(profitDecimalToPercent(-0.3)).toBe(0);
});
it("round-trips representative storage values", () => {
for (const decimal of [0.05, 0.1, 0.3, 0.3333, 0.5, 0.75, 0.9999]) {
expect(profitPercentToDecimal(profitDecimalToPercent(decimal))).toBe(
decimal,
);
}
});
});
describe("validateProfitControlFormState", () => {
it("passes valid enabled configurations", () => {
expect(validateProfitControlFormState(formState())).toBeNull();
expect(
validateProfitControlFormState(
formState({
profit_min_margin_percent: 0,
profit_safety_buffer_percent: 0,
}),
),
).toBeNull();
expect(
validateProfitControlFormState(
formState({
profit_min_margin_percent: 60,
profit_safety_buffer_percent: 39.99,
}),
),
).toBeNull();
});
it("validates all five supported platforms and skips unsupported platforms", () => {
expect(
validateProfitControlFormState(
formState({ profit_control_enabled: false, profit_min_margin_percent: 200 }),
),
).toBeNull();
expect(
validateProfitControlFormState(
formState({ platform: "anthropic", profit_min_margin_percent: 200 }),
),
).toBe("marginRangeError");
for (const platform of ["openai", "anthropic", "gemini", "grok", "antigravity"]) {
expect(validateProfitControlFormState(formState({ platform }))).toBeNull();
}
expect(
validateProfitControlFormState(
formState({ platform: "composite", profit_min_margin_percent: 200 }),
),
).toBeNull();
});
it("treats empty inputs as zero", () => {
expect(
validateProfitControlFormState(
formState({
profit_min_margin_percent: "",
profit_safety_buffer_percent: null,
}),
),
).toBeNull();
});
it("rejects out-of-range margin and buffer", () => {
expect(
validateProfitControlFormState(
formState({ profit_min_margin_percent: 100 }),
),
).toBe("marginRangeError");
expect(
validateProfitControlFormState(
formState({ profit_min_margin_percent: -1 }),
),
).toBe("marginRangeError");
expect(
validateProfitControlFormState(
formState({ profit_safety_buffer_percent: 100 }),
),
).toBe("bufferRangeError");
expect(
validateProfitControlFormState(
formState({ profit_safety_buffer_percent: -0.1 }),
),
).toBe("bufferRangeError");
});
it("rejects margin plus buffer reaching 100 percent", () => {
expect(
validateProfitControlFormState(
formState({
profit_min_margin_percent: 60,
profit_safety_buffer_percent: 40,
}),
),
).toBe("sumTooHigh");
});
});
@@ -0,0 +1,56 @@
// 分组利润控制表单辅助:百分比 <-> 小数换算与提交前校验。
// 后端按小数存 decimal(10,4)(0.30 = 30%),界面按百分比输入展示;
// 固定 4 位小数精度,避免 0.3 * 100 = 30.000000000000004 之类的浮点尾数回显。
export const profitPercentToDecimal = (
value: number | string | null | undefined,
): number => {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
return 0;
}
return Math.round(parsed * 100) / 10000;
};
export const profitDecimalToPercent = (
value: number | null | undefined,
): number => {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
return 0;
}
return Math.round(parsed * 1e6) / 1e4;
};
export type ProfitControlFormState = {
platform: string;
profit_control_enabled: boolean;
profit_min_margin_percent: number | string | null;
profit_safety_buffer_percent: number | string | null;
};
export const isProfitControlPlatform = (platform: string): boolean =>
["openai", "anthropic", "gemini", "grok", "antigravity"].includes(platform);
// 提交前校验:margin/buffer 各自 ∈ [0,100),且相加 < 100(否则阈值 <= 0,
// 所有可核价账号都会被排除)。返回 null 表示通过,否则返回错误信息的 i18n key
//(相对 admin.groups.profitControl 前缀)。仅支持平台且开关开启时才校验。
export const validateProfitControlFormState = (
form: ProfitControlFormState,
): string | null => {
if (!isProfitControlPlatform(form.platform) || !form.profit_control_enabled) {
return null;
}
const margin = Number(form.profit_min_margin_percent || 0);
const buffer = Number(form.profit_safety_buffer_percent || 0);
if (!Number.isFinite(margin) || margin < 0 || margin >= 100) {
return "marginRangeError";
}
if (!Number.isFinite(buffer) || buffer < 0 || buffer >= 100) {
return "bufferRangeError";
}
if (margin + buffer >= 100) {
return "sumTooHigh";
}
return null;
};