diff --git a/cli/src/__tests__/cost.test.ts b/cli/src/__tests__/cost.test.ts new file mode 100644 index 00000000000..480cb994c67 --- /dev/null +++ b/cli/src/__tests__/cost.test.ts @@ -0,0 +1,29 @@ +import { Command } from "commander"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { registerCostCommands } from "../commands/client/cost.js"; + +function program() { + const result = new Command().exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }); + registerCostCommands(result); + return result; +} +const context = ["--company-id", "test-company", "--api-base", "http://paperclip.test", "--api-key", "test-key", "--json"]; +afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks(); }); +describe("cost report date options", () => { + it.each([["cost", "summary", "costs/summary"], ["finance", "events", "costs/finance-events"]])("forwards bounds and all-time for %s %s", async (group, name, path) => { + const fetch = vi.fn().mockImplementation(() => Promise.resolve(Response.json({}))); + vi.stubGlobal("fetch", fetch); + vi.spyOn(console, "log").mockImplementation(() => {}); + await program().parseAsync([group, name, ...context, "--all-time"], { from: "user" }); + expect(fetch.mock.calls[0][0]).toBe(`http://paperclip.test/api/companies/test-company/${path}?period=all`); + await program().parseAsync([group, name, ...context, "--from", "2026-09-01", "--to", "2026-09-02"], { from: "user" }); + expect(fetch.mock.calls[1][0]).toBe(`http://paperclip.test/api/companies/test-company/${path}?from=2026-09-01&to=2026-09-02`); + }); + it.each([["accounting", "health"], ["accounting", "inspect"], ["accounting", "invoices"], ["budget", "overview"], ["cost", "window-spend"], ["cost", "quota-windows"]])("rejects unsupported date flags for %s %s", async (group, name) => { + const fetch = vi.fn(); vi.stubGlobal("fetch", fetch); + for (const args of [["--all-time"], ["--from", "2026-09-01"], ["--to", "2026-09-02"]]) { + await expect(program().parseAsync([group, name, ...context, ...args], { from: "user" })).rejects.toThrow("unknown option"); + } + expect(fetch).not.toHaveBeenCalled(); + }); +}); diff --git a/cli/src/commands/client/cost.ts b/cli/src/commands/client/cost.ts index 38f0e88a513..32fb420443e 100644 --- a/cli/src/commands/client/cost.ts +++ b/cli/src/commands/client/cost.ts @@ -36,7 +36,7 @@ export function registerCostCommands(program: Command): void { ["window-spend", "costs/window-spend"], ["quota-windows", "costs/quota-windows"], ] as const) { - addCompanyGet(cost, name, `Get ${name} cost data`, path); + addCompanyGet(cost, name, `Get ${name} cost data`, path, name !== "window-spend" && name !== "quota-windows"); } addCommonClientOptions( @@ -80,10 +80,10 @@ export function registerCostCommands(program: Command): void { const finance = program.command("finance").description("Finance event and summary operations"); addCompanyPostJson(finance, "event:create", "Record a finance event", "finance-events"); - addCompanyGet(finance, "events", "List finance events", "costs/finance-events"); - addCompanyGet(finance, "summary", "Get finance summary", "costs/finance-summary"); - addCompanyGet(finance, "by-biller", "Get finance summary by biller", "costs/finance-by-biller"); - addCompanyGet(finance, "by-kind", "Get finance summary by kind", "costs/finance-by-kind"); + addCompanyGet(finance, "events", "List finance events", "costs/finance-events", true); + addCompanyGet(finance, "summary", "Get finance summary", "costs/finance-summary", true); + addCompanyGet(finance, "by-biller", "Get finance summary by biller", "costs/finance-by-biller", true); + addCompanyGet(finance, "by-kind", "Get finance summary by kind", "costs/finance-by-kind", true); const budget = program.command("budget").description("Budget policy and incident operations"); addCompanyGet(budget, "overview", "Get budget overview", "budgets/overview"); @@ -147,16 +147,16 @@ export function registerCostCommands(program: Command): void { ); } -function addCompanyGet(parent: Command, name: string, description: string, path: string): void { - addCommonClientOptions( - parent - .command(name) - .description(description) - .option("-C, --company-id ", "Company ID") +function addCompanyGet(parent: Command, name: string, description: string, path: string, dateRange = false): void { + const command = parent.command(name).description(description).option("-C, --company-id ", "Company ID"); + if (dateRange) { + command .option("--from ", "Inclusive start date (default: current UTC month)") .option("--to ", "Inclusive end date") - .option("--all-time", "Report all recorded history") - .action(async (opts: CompanyOptions) => { + .option("--all-time", "Report all recorded history"); + } + addCommonClientOptions( + command.action(async (opts: CompanyOptions) => { try { const ctx = resolveCommandContext(opts, { requireCompany: true }); const params = new URLSearchParams(); diff --git a/doc/COST-ACCOUNTING.md b/doc/COST-ACCOUNTING.md index f3c10b14bb6..c315a448d5e 100644 --- a/doc/COST-ACCOUNTING.md +++ b/doc/COST-ACCOUNTING.md @@ -40,7 +40,7 @@ Reservations are estimates, not provider-enforced spending caps. Delayed receipt ## Dates, completeness, and UI -Cost and finance endpoints default to the current UTC month through now. Explicit `from`/`to` bounds remain inclusive; `period=all` requests all time. Reversed or malformed bounds are rejected. CLI cost/finance reads expose `--from`, `--to`, and `--all-time`. The board sends explicit ranges, and month/year-to-date starts use UTC. Rolling spend excludes future events. +Cost and finance endpoints default to the current UTC month through now. Explicit `from`/`to` bounds remain inclusive; `period=all` requests all time. Reversed or malformed bounds are rejected. Date-filterable CLI cost/finance reports expose `--from`, `--to`, and `--all-time`. Operational accounting, budget, live quota and rolling-window commands reject these flags because their endpoints do not filter by arbitrary dates. The board sends explicit ranges, and month/year-to-date starts use UTC. Rolling spend excludes future events. The dashboard compares spend with the monthly cap only for Month to Date; historical ranges show the monthly limit without a utilization percentage. Provider report imports default to the previous complete UTC day, with an exclusive end date. Cost summaries expose `eventCount`, `estimatedEventCount`, `unpricedEventCount`, `pendingRunCount`, and `pricingComplete`. Estimated charges count toward budgets, while the UI distinguishes estimates from provider-reported dollars. Agent totals and expanded model rows show **Estimated** when every charge in that group is estimated, or **Partially estimated** for a mix. Groups with no estimates have no estimate badge. These labels follow the selected date range and use ledger-event counts, not run counts; they do not infer pricing status from the provider or model name. `pricingComplete` means no missing prices or pending receipts; it does not mean invoice reconciliation has occurred. The Costs page warns when totals are incomplete. Provider cards include cached tokens and calculate subscription shares using each token once. Provider spend is compared with the real company budget, not a fabricated proportional allocation. Run displays prefer cache-adjusted prices and preserve explicit zeroes. diff --git a/ui/src/components/BillerSpendCard.tsx b/ui/src/components/BillerSpendCard.tsx index a2ea2d2eb1d..db6aa467c9e 100644 --- a/ui/src/components/BillerSpendCard.tsx +++ b/ui/src/components/BillerSpendCard.tsx @@ -8,6 +8,8 @@ interface BillerSpendCardProps { row: CostByBiller; weekSpendCents: number; budgetMonthlyCents: number; + /** Only compare a current-month report with the monthly budget. */ + showBudgetUtilization?: boolean; totalCompanySpendCents: number; providerRows: CostByProviderModel[]; } @@ -16,6 +18,7 @@ export function BillerSpendCard({ row, weekSpendCents, budgetMonthlyCents, + showBudgetUtilization = true, totalCompanySpendCents, providerRows, }: BillerSpendCardProps) { @@ -44,10 +47,7 @@ export function BillerSpendCard({ return Array.from(map.entries()).sort((a, b) => b[1] - a[1]); }, [providerRows]); - const providerBudgetShare = - budgetMonthlyCents > 0 && totalCompanySpendCents > 0 - ? (row.costCents / totalCompanySpendCents) * budgetMonthlyCents - : budgetMonthlyCents; + const providerBudgetShare = budgetMonthlyCents; const budgetPct = providerBudgetShare > 0 ? Math.min(100, (row.costCents / providerBudgetShare) * 100) @@ -78,12 +78,12 @@ export function BillerSpendCard({ - {budgetMonthlyCents > 0 && ( + {showBudgetUtilization && budgetMonthlyCents > 0 && ( )} diff --git a/ui/src/components/FinancialEventEntry.test.tsx b/ui/src/components/FinancialEventEntry.test.tsx index efa80eb5a01..55751ec06cd 100644 --- a/ui/src/components/FinancialEventEntry.test.tsx +++ b/ui/src/components/FinancialEventEntry.test.tsx @@ -199,6 +199,25 @@ describe("financial event entry", () => { expect(mocks.provider).toHaveBeenCalledWith("company-one", { provider: "anthropic", secretId, accountId: "org_example", scopeIds: ["workspace_one", "workspace_two"], from: "2026-09-01", to: "2026-09-02" }); expect(document.querySelector("form")).toBeNull(); }); + it("defaults provider reports to the previous complete UTC day and explains invalid ranges", async () => { + const secretId = "00000000-0000-4000-8000-000000000001"; + mocks.secrets.mockResolvedValue([{ id: secretId, name: "Admin", scope: "company" }]); + const today = new Date().toISOString().slice(0, 10); + const yesterday = new Date(Date.now() - 86_400_000).toISOString().slice(0, 10); + await render(); await click("Record or import charges"); await click("Provider report"); + const dates = [...document.querySelectorAll('input[type="date"]')].map(input => input.value); + expect(dates).toEqual([yesterday, today]); + await fill("Company admin credential", secretId); + await fill("Provider organization ID", "org_example"); + await fill("Project IDs", "project_one"); + await fill("From (UTC)", today); + await submit(); + expect(mocks.provider).not.toHaveBeenCalled(); + expect(document.body.textContent).toContain("End date must be after start date (exclusive)."); + await fill("From (UTC)", yesterday); + await submit(); + expect(mocks.provider).toHaveBeenCalledWith("company-one", expect.objectContaining({ from: yesterday, to: today })); + }); it("clears an open draft when changing company", async () => { await render(); await click("Record or import charges"); diff --git a/ui/src/components/FinancialEventEntry.tsx b/ui/src/components/FinancialEventEntry.tsx index 0a1ad456ba9..e8463567d24 100644 --- a/ui/src/components/FinancialEventEntry.tsx +++ b/ui/src/components/FinancialEventEntry.tsx @@ -41,6 +41,7 @@ function Entry({ companyId }: { companyId: string }) { const [secretId, setSecretId] = useState(""); const [accountId, setAccountId] = useState(""); const [scopes, setScopes] = useState(""); + const [providerFrom, setProviderFrom] = useState(() => new Date(Date.now() - 86_400_000).toISOString().slice(0, 10)); const [endDate, setEndDate] = useState(new Date().toISOString().slice(0, 10)); const secrets = useQuery({ queryKey: ["billing-credentials", companyId], @@ -52,6 +53,10 @@ function Entry({ companyId }: { companyId: string }) { const action = useMutation({ mutationFn: async () => { if (mode === "provider") { + if (providerFrom && endDate && providerFrom >= endDate) { + setValidation("End date must be after start date (exclusive)."); + return false; + } const input = importProviderCostsSchema.safeParse({ provider, secretId, @@ -60,7 +65,7 @@ function Entry({ companyId }: { companyId: string }) { .split(",") .map((s) => s.trim()) .filter(Boolean), - from: date, + from: providerFrom, to: endDate, }); if (!input.success) { @@ -306,8 +311,8 @@ function Entry({ companyId }: { companyId: string }) { From (UTC) setDate(e.target.value)} + value={providerFrom} + onChange={(e) => setProviderFrom(e.target.value)} required disabled={action.isPending} /> diff --git a/ui/src/components/ProviderQuotaCard.test.tsx b/ui/src/components/ProviderQuotaCard.test.tsx index e33b109bed5..6555c5144f8 100644 --- a/ui/src/components/ProviderQuotaCard.test.tsx +++ b/ui/src/components/ProviderQuotaCard.test.tsx @@ -1,5 +1,6 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; +import { BillerSpendCard } from "./BillerSpendCard"; import { ProviderQuotaCard } from "./ProviderQuotaCard"; describe("provider cost shares", () => { @@ -14,4 +15,21 @@ describe("provider cost shares", () => { expect(html).toContain("50% of company budget"); expect(html).not.toContain("of allocation"); }); + it("hides historical spend-to-monthly-budget comparisons for providers and billers", () => { + const common = { budgetMonthlyCents: 1000, totalCompanySpendCents: 20000, weekSpendCents: 0, showBudgetUtilization: false }; + const provider = renderToStaticMarkup(); + const biller = renderToStaticMarkup(); + for (const html of [provider, biller]) { + expect(html).not.toContain("Period spend"); + expect(html).not.toContain("of allocation"); + expect(html).not.toContain("of company budget"); + } + }); + + it("compares biller spend with the actual company cap", () => { + const html = renderToStaticMarkup(); + expect(html).toContain("20% of company budget"); + expect(html).not.toContain("80% of allocation"); + }); + }); diff --git a/ui/src/components/ProviderQuotaCard.tsx b/ui/src/components/ProviderQuotaCard.tsx index 4f81e4c892c..0c770499b4e 100644 --- a/ui/src/components/ProviderQuotaCard.tsx +++ b/ui/src/components/ProviderQuotaCard.tsx @@ -23,6 +23,8 @@ interface ProviderQuotaCardProps { rows: CostByProviderModel[]; /** company monthly budget in cents (0 means unlimited) */ budgetMonthlyCents: number; + /** Only compare a current-month report with the monthly budget. */ + showBudgetUtilization?: boolean; /** total company spend in this period in cents, all providers */ totalCompanySpendCents: number; /** spend in the current calendar week in cents, this provider only */ @@ -37,13 +39,13 @@ interface ProviderQuotaCardProps { quotaError?: string | null; quotaSource?: string | null; quotaLoading?: boolean; - quotaAccounts?: ProviderQuotaResult[]; } export function ProviderQuotaCard({ provider, rows, budgetMonthlyCents, + showBudgetUtilization = true, totalCompanySpendCents, weekSpendCents, windowRows, @@ -54,7 +56,6 @@ export function ProviderQuotaCard({ quotaError = null, quotaSource = null, quotaLoading = false, - quotaAccounts, }: ProviderQuotaCardProps) { // single-pass aggregation over rows — memoized so the 8 derived values are not // recomputed on every parent render tick (providers tab polls every 30s, and each @@ -115,7 +116,7 @@ export function ProviderQuotaCard({ const weekPct = weeklyBudgetShare > 0 ? Math.min(100, (weekSpendCents / weeklyBudgetShare) * 100) : 0; - const hasBudget = budgetMonthlyCents > 0; + const hasBudget = showBudgetUtilization && budgetMonthlyCents > 0; // memoized so the Map and max are not reconstructed on every parent render tick const windowMap = useMemo( diff --git a/ui/src/pages/Costs.test.tsx b/ui/src/pages/Costs.test.tsx index 318afd78a96..81f63841e00 100644 --- a/ui/src/pages/Costs.test.tsx +++ b/ui/src/pages/Costs.test.tsx @@ -123,12 +123,30 @@ describe("Shared Costs surfaces", () => { await act(async () => { await vi.waitFor(() => expect(upsertPolicyMock).toHaveBeenCalled()); }); expect(upsertPolicyMock).toHaveBeenCalledWith("company-1", { scopeType: summary.scopeType, scopeId: summary.scopeId, metric: summary.metric, windowKind: summary.windowKind, - amount: field === "amount" ? 100 : 500, warnPercent: 65, hardStopEnabled: false, notifyEnabled: false, isActive: false, - unpricedUsagePolicy: field === "unknown price" ? "block" : "allow", reservationCents: field === "reservation" ? "100.0000000" : "2.0000000", + ...(field === "amount" ? { amount: 100 } : field === "unknown price" ? { unpricedUsagePolicy: "block" } : { reservationCents: "100.0000000" }), }); queryClient.clear(); }); + it("compares monthly budgets only with month-to-date spend", async () => { + for (const mock of Object.values(costsApiMocks)) mock.mockResolvedValue([]); + costsApiMocks.summary.mockResolvedValue({ spendCents: 200, budgetCents: 1000, utilizationPercent: 20, pricingComplete: true }); + costsApiMocks.financeSummary.mockResolvedValue({ netCents: 0, debitCents: 0, creditCents: 0, estimatedDebitCents: 0, eventCount: 0 }); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + root = createRoot(container); + await act(async () => root.render()); + await act(async () => { await vi.waitFor(() => expect(container.textContent).toContain("20% of monthly budget consumed this month")); }); + costsApiMocks.summary.mockResolvedValue({ spendCents: 12000, budgetCents: 1000, utilizationPercent: 1200, pricingComplete: true }); + await act(async () => [...container.querySelectorAll("button")].find(b => b.textContent === "All Time")!.click()); + await act(async () => { await vi.waitFor(() => expect(container.textContent).toContain("$120.00")); }); + expect(container.textContent).toContain("Monthly budget $10.00"); + expect(container.textContent).toContain("Monthly limit"); + expect(container.textContent).not.toContain("1200%"); + expect(container.textContent).not.toContain("of monthly budget consumed"); + expect(costsApiMocks.summary).toHaveBeenLastCalledWith("company-1", undefined, undefined); + queryClient.clear(); + }); + it.each(surfaces)("shows incomplete accounting and currency boundaries on the %s page", async (_name, props) => { for (const mock of Object.values(costsApiMocks)) mock.mockResolvedValue([]); costsApiMocks.summary.mockResolvedValue({ spendCents: 12.4, budgetCents: 0, pricingComplete: false, unpricedEventCount: 2, pendingRunCount: 1 }); diff --git a/ui/src/pages/Costs.tsx b/ui/src/pages/Costs.tsx index b4c6cca8a65..a1eda46eaf1 100644 --- a/ui/src/pages/Costs.tsx +++ b/ui/src/pages/Costs.tsx @@ -246,13 +246,6 @@ export function Costs({ scopeId: summary.scopeId, metric: summary.metric, windowKind: summary.windowKind, - amount: summary.amount, - warnPercent: summary.warnPercent, - hardStopEnabled: summary.hardStopEnabled, - notifyEnabled: summary.notifyEnabled, - isActive: summary.isActive, - unpricedUsagePolicy: summary.unpricedUsagePolicy, - reservationCents: summary.reservationCents, ...changes, }), onSuccess: invalidateBudgetViews, @@ -651,14 +644,14 @@ export function Costs({ label="Budget" value={activeBudgetIncidents.length > 0 ? String(activeBudgetIncidents.length) : ( spendData?.summary.budgetCents && spendData.summary.budgetCents > 0 - ? `${spendData.summary.utilizationPercent}%` + ? preset === "mtd" ? `${spendData.summary.utilizationPercent}%` : formatCents(spendData.summary.budgetCents) : "Open" )} subtitle={ activeBudgetIncidents.length > 0 ? `${budgetData?.pausedAgentCount ?? 0} agents paused · ${budgetData?.pausedProjectCount ?? 0} projects paused` : spendData?.summary.budgetCents && spendData.summary.budgetCents > 0 - ? `${formatCents(spendData.summary.spendCents)} of ${formatCents(spendData.summary.budgetCents)}` + ? preset === "mtd" ? `${formatCents(spendData.summary.spendCents)} of ${formatCents(spendData.summary.budgetCents)} this month` : "Monthly limit" : "No monthly cap configured" } icon={Coins} @@ -741,7 +734,7 @@ export function Costs({
{spendData?.summary.budgetCents && spendData.summary.budgetCents > 0 - ? `Budget ${formatCents(spendData.summary.budgetCents)}` + ? `Monthly budget ${formatCents(spendData.summary.budgetCents)}` : "Unlimited budget"}
@@ -752,7 +745,7 @@ export function Costs({ - {spendData?.summary.budgetCents && spendData.summary.budgetCents > 0 ? ( + {preset === "mtd" && spendData?.summary.budgetCents && spendData.summary.budgetCents > 0 ? (
- {spendData.summary.utilizationPercent}% of monthly budget consumed in this range. + {spendData.summary.utilizationPercent}% of monthly budget consumed this month.
) : null} @@ -1049,13 +1042,14 @@ export function Costs({ provider={provider} rows={byProvider.get(provider) ?? []} budgetMonthlyCents={spendData?.summary.budgetCents ?? 0} + showBudgetUtilization={preset === "mtd"} totalCompanySpendCents={spendData?.summary.spendCents ?? 0} weekSpendCents={weekSpendByProvider.get(provider) ?? 0} windowRows={windowSpendByProvider.get(provider) ?? []} showDeficitNotch={deficitNotchByProvider.get(provider) ?? false} quotaAccounts={quotaData?.filter(account => account.provider === provider)} - quotaRequestFailed={Boolean(quotaFetchError)} - quotaWindows={quotaWindowsByProvider.get(provider) ?? []} + quotaRequestFailed={Boolean(quotaFetchError)} + quotaWindows={quotaWindowsByProvider.get(provider) ?? []} quotaError={quotaErrorsByProvider.get(provider) ?? null} quotaSource={quotaSourcesByProvider.get(provider) ?? null} quotaLoading={quotaLoading} @@ -1071,13 +1065,14 @@ export function Costs({ provider={provider} rows={byProvider.get(provider) ?? []} budgetMonthlyCents={spendData?.summary.budgetCents ?? 0} + showBudgetUtilization={preset === "mtd"} totalCompanySpendCents={spendData?.summary.spendCents ?? 0} weekSpendCents={weekSpendByProvider.get(provider) ?? 0} windowRows={windowSpendByProvider.get(provider) ?? []} showDeficitNotch={deficitNotchByProvider.get(provider) ?? false} quotaAccounts={quotaData?.filter(account => account.provider === provider)} - quotaRequestFailed={Boolean(quotaFetchError)} - quotaWindows={quotaWindowsByProvider.get(provider) ?? []} + quotaRequestFailed={Boolean(quotaFetchError)} + quotaWindows={quotaWindowsByProvider.get(provider) ?? []} quotaError={quotaErrorsByProvider.get(provider) ?? null} quotaSource={quotaSourcesByProvider.get(provider) ?? null} quotaLoading={quotaLoading} @@ -1112,6 +1107,7 @@ export function Costs({ row={row} weekSpendCents={weekSpendByBiller.get(biller) ?? 0} budgetMonthlyCents={spendData?.summary.budgetCents ?? 0} + showBudgetUtilization={preset === "mtd"} totalCompanySpendCents={spendData?.summary.spendCents ?? 0} providerRows={providerRows} /> @@ -1131,6 +1127,7 @@ export function Costs({ row={row} weekSpendCents={weekSpendByBiller.get(biller) ?? 0} budgetMonthlyCents={spendData?.summary.budgetCents ?? 0} + showBudgetUtilization={preset === "mtd"} totalCompanySpendCents={spendData?.summary.spendCents ?? 0} providerRows={providerRows} /> diff --git a/ui/storybook/fixtures/costsFinance.test.ts b/ui/storybook/fixtures/costsFinance.test.ts index a362a9099c7..d65b6dd1c01 100644 --- a/ui/storybook/fixtures/costsFinance.test.ts +++ b/ui/storybook/fixtures/costsFinance.test.ts @@ -30,4 +30,16 @@ describe("Costs story finance actions", () => { const summary = await (await fixture("costs/finance-summary", request("costs/finance-summary")))!.json(); expect(summary).toMatchObject({ debitCents: 50, debitCentsExact: "50.0000000", providerReportedCents: 250, providerReportedCentsExact: "250.0000000", eventCount: 2 }); }); + it("keeps timestamp bounds exact while date-only bounds include the full day", async () => { + const fixture = createCostsFinanceFixtures("preview"); + for (const [hour, id] of [["12", "at-boundary"], ["18", "after-boundary"]]) { + await fixture("finance-events", request("finance-events", { ...charge, idempotencyKey: id, occurredAt: `2026-09-01T${hour}:00:00Z` })); + } + const exact = await (await fixture("costs/finance-events", request("costs/finance-events?to=2026-09-01T12:00:00Z")))!.json(); + expect(exact).toHaveLength(1); + expect(exact[0].occurredAt).toBe("2026-09-01T12:00:00.000Z"); + const wholeDay = await (await fixture("costs/finance-events", request("costs/finance-events?to=2026-09-01")))!.json(); + expect(wholeDay).toHaveLength(2); + }); + }); diff --git a/ui/storybook/fixtures/costsFinance.ts b/ui/storybook/fixtures/costsFinance.ts index 1ebd20a61e3..d24eede5871 100644 --- a/ui/storybook/fixtures/costsFinance.ts +++ b/ui/storybook/fixtures/costsFinance.ts @@ -34,7 +34,8 @@ export function createCostsFinanceFixtures(companyId: string) { return async (resource: string, request: Request): Promise => { const url = new URL(request.url); const from = url.searchParams.get("from"); const to = url.searchParams.get("to"); - const visible = events.filter(row => (!from || row.occurredAt >= new Date(from)) && (!to || row.occurredAt <= new Date(`${to.slice(0, 10)}T23:59:59.999Z`))); + const until = to ? new Date(/^\d{4}-\d{2}-\d{2}$/.test(to) ? `${to}T23:59:59.999Z` : to) : null; + const visible = events.filter(row => (!from || row.occurredAt >= new Date(from)) && (!until || row.occurredAt <= until)); if (resource === "costs/finance-summary") return Response.json({ companyId, ...summary(visible), eventCount: visible.length, currencies: [...new Set(visible.map(row => row.currency))].map(currency => summary(visible, currency)) }); if (resource === "costs/finance-events") return Response.json(visible); if (resource === "costs/finance-by-biller" || resource === "costs/finance-by-kind") {