Fix budget incident resolution edge cases

This commit is contained in:
Dotta
2026-03-16 16:48:13 -05:00
parent 1990b29018
commit 8fbbc4ada6
7 changed files with 9259 additions and 7 deletions

View File

@@ -218,4 +218,94 @@ describe("budgetService", () => {
reason: "Company is paused because its budget hard-stop was reached.",
});
});
it("uses live observed spend when raising a budget incident", async () => {
const dbStub = createDbStub([
[{
id: "incident-1",
companyId: "company-1",
policyId: "policy-1",
amountObserved: 120,
approvalId: "approval-1",
}],
[{
id: "policy-1",
companyId: "company-1",
scopeType: "company",
scopeId: "company-1",
metric: "billed_cents",
windowKind: "calendar_month_utc",
}],
[{ total: 150 }],
]);
const service = budgetService(dbStub.db as any);
await expect(
service.resolveIncident(
"company-1",
"incident-1",
{ action: "raise_budget_and_resume", amount: 140 },
"board-user",
),
).rejects.toThrow("New budget must exceed current observed spend");
});
it("syncs company monthly budget when raising and resuming a company incident", async () => {
const now = new Date();
const dbStub = createDbStub([
[{
id: "incident-1",
companyId: "company-1",
policyId: "policy-1",
scopeType: "company",
scopeId: "company-1",
metric: "billed_cents",
windowKind: "calendar_month_utc",
windowStart: now,
windowEnd: now,
thresholdType: "hard",
amountLimit: 100,
amountObserved: 120,
status: "open",
approvalId: "approval-1",
resolvedAt: null,
createdAt: now,
updatedAt: now,
}],
[{
id: "policy-1",
companyId: "company-1",
scopeType: "company",
scopeId: "company-1",
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: 100,
}],
[{ total: 120 }],
[{ id: "approval-1", status: "approved" }],
[{
companyId: "company-1",
name: "Paperclip",
status: "paused",
pauseReason: "budget",
pausedAt: now,
}],
]);
const service = budgetService(dbStub.db as any);
await service.resolveIncident(
"company-1",
"incident-1",
{ action: "raise_budget_and_resume", amount: 175 },
"board-user",
);
expect(dbStub.updateSet).toHaveBeenCalledWith(
expect.objectContaining({
budgetMonthlyCents: 175,
updatedAt: expect.any(Date),
}),
);
});
});

View File

@@ -878,24 +878,33 @@ export function budgetService(db: Db, hooks: BudgetServiceHooks = {}) {
const policy = await getPolicyRow(incident.policyId);
if (input.action === "raise_budget_and_resume") {
const nextAmount = Math.max(0, Math.floor(input.amount ?? 0));
if (nextAmount <= incident.amountObserved) {
const currentObserved = await computeObservedAmount(db, policy);
if (nextAmount <= currentObserved) {
throw unprocessable("New budget must exceed current observed spend");
}
const now = new Date();
await db
.update(budgetPolicies)
.set({
amount: nextAmount,
isActive: true,
updatedByUserId: actorUserId,
updatedAt: new Date(),
updatedAt: now,
})
.where(eq(budgetPolicies.id, policy.id));
if (policy.scopeType === "company" && policy.windowKind === "calendar_month_utc") {
await db
.update(companies)
.set({ budgetMonthlyCents: nextAmount, updatedAt: now })
.where(eq(companies.id, policy.scopeId));
}
if (policy.scopeType === "agent" && policy.windowKind === "calendar_month_utc") {
await db
.update(agents)
.set({ budgetMonthlyCents: nextAmount, updatedAt: new Date() })
.set({ budgetMonthlyCents: nextAmount, updatedAt: now })
.where(eq(agents.id, policy.scopeId));
}
@@ -904,8 +913,8 @@ export function budgetService(db: Db, hooks: BudgetServiceHooks = {}) {
.update(budgetIncidents)
.set({
status: "resolved",
resolvedAt: new Date(),
updatedAt: new Date(),
resolvedAt: now,
updatedAt: now,
})
.where(and(eq(budgetIncidents.policyId, policy.id), eq(budgetIncidents.status, "open")));