Fix budget incident resolution edge cases
This commit is contained in:
@@ -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),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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")));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user