fix(ui): align customer member budget form with API contract (#51523)

This commit is contained in:
tin-berri 2026-11-12 18:28:53 -08:00 • committed by GitHub
parent 78db4f1662
commit f85aa27c16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 50 additions and 6 deletions

View file

@ -35,7 +35,7 @@ const member: Member = {
customer_member_id: "member-1",
parent_team_id: "team-1",
alias: "Member One",
budget_template_id: "template-1",
budget_id: "template-1",
budget_override: { max_budget: 20 },
effective_budget: { max_budget: 20 },
budget_override_expires_at: "2026-12-01T12:00:00Z",
@ -116,13 +116,31 @@ describe("CustomerHierarchySettings", () => {
expect(template).toHaveTextContent("Pro");
await user.click(screen.getByRole("button", { name: "Save changes" }));
await waitFor(() => expect(onSaved).toHaveBeenCalledOnce());
expect(await lastRequest().clone().json()).toEqual({ budget_template_id: "template-2" });
expect(new URL(lastRequest().url).pathname).toBe("/management/v1/customer_members/member-1");
expect(await lastRequest().clone().json()).toEqual({ budget_id: "template-2" });
});
it("should clear the template without changing the existing override", async () => {
const user = userEvent.setup();
fetchMock.mockResolvedValue(json({ ...member, budget_id: null }));
render(
<CustomerHierarchySettings
entity={{ kind: "member", record: member }}
organization={organization}
onSaved={onSaved}
onDeleted={onDeleted}
/>,
);
await chooseSelectOption(user, screen.getByRole("combobox", { name: "Budget template" }), "No template");
await user.click(screen.getByRole("button", { name: "Save changes" }));
await waitFor(() => expect(onSaved).toHaveBeenCalledOnce());
expect(await lastRequest().clone().json()).toEqual({ budget_id: null });
});
it("should identify an unavailable stored template instead of displaying No template", () => {
render(
<CustomerHierarchySettings
entity={{ kind: "member", record: { ...member, budget_template_id: "retired-template" } }}
entity={{ kind: "member", record: { ...member, budget_id: "retired-template" } }}
organization={organization}
onSaved={onSaved}
onDeleted={onDeleted}

View file

@ -33,13 +33,39 @@ const member: Member = {
customer_member_id: "member-1",
parent_team_id: "team-1",
alias: "Member One",
budget_template_id: "template-1",
budget_id: "template-1",
budget_override: { max_budget: 20, rpm_limit: 4 },
effective_budget: { max_budget: 20, rpm_limit: 4 },
budget_override_expires_at: "2026-12-01T12:00:00Z",
};
describe("customer hierarchy settings payloads", () => {
it("should load the member template from the API response", () => {
expect(customerHierarchyFields({ kind: "member", record: member }).templateId).toBe(member.budget_id);
});
it("should allow an alias-only edit for a member using a template without an override", () => {
const entity: CustomerHierarchyEntity = {
kind: "member",
record: { ...member, budget_override: null, budget_override_expires_at: null },
};
expect(buildCustomerHierarchyPatch(entity, { ...customerHierarchyFields(entity), alias: "Updated" })).toEqual({
ok: true,
patch: { kind: "member", body: { alias: "Updated" } },
});
});
it.each([
["template-2", "template-2"],
["", null],
])("should submit template selection %s without changing the override", (templateId, budgetId) => {
const entity: CustomerHierarchyEntity = { kind: "member", record: member };
expect(buildCustomerHierarchyPatch(entity, { ...customerHierarchyFields(entity), templateId })).toEqual({
ok: true,
patch: { kind: "member", body: { budget_id: budgetId } },
});
});
it("should leave untouched settings out of the patch", () => {
const entity: CustomerHierarchyEntity = { kind: "organization", record: organization };
expect(buildCustomerHierarchyPatch(entity, customerHierarchyFields(entity))).toEqual({

View file

@ -66,7 +66,7 @@ export const customerHierarchyFields = (entity: CustomerHierarchyEntity): Custom
tpm: asText(budget?.tpm_limit),
rpm: asText(budget?.rpm_limit),
allowedModels: budget?.allowed_models?.join("\n") ?? "",
templateId: entity.kind === "member" ? entity.record.budget_template_id ?? "" : "",
templateId: entity.kind === "member" ? entity.record.budget_id ?? "" : "",
overrideExpiry: entity.kind === "member" ? localDateTime(entity.record.budget_override_expires_at) : "",
};
};
@ -125,7 +125,7 @@ const memberPatch = (
const initial = customerHierarchyFields({ kind: "member", record: member });
if (!fields.templateId && !fields.budgetEnabled) return { ok: false, error: "budgetRequired" };
const body: components["schemas"]["MemberPatch"] = aliasChanges(member.alias, fields.alias);
if (fields.templateId !== initial.templateId) body.budget_template_id = fields.templateId || null;
if (fields.templateId !== initial.templateId) body.budget_id = fields.templateId || null;
if (changed) body.budget_override = budget;
if (!fields.budgetEnabled && initial.budgetEnabled) body.budget_override_expires_at = null;
if (fields.budgetEnabled && fields.overrideExpiry !== initial.overrideExpiry) {