Skip to content

Commit 23516e8

Browse files
asizikovCopilot
andcommitted
feat: enable native budget simulation UI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6bd6a5d commit 23516e8

2 files changed

Lines changed: 121 additions & 56 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { createElement } from 'react'
2+
import { renderToStaticMarkup } from 'react-dom/server'
3+
import { describe, expect, it, vi } from 'vitest'
4+
5+
import type { BudgetSimulationResult } from '../utils/budgetSimulation'
6+
import { EMPTY_BUDGET_VALUES, type BudgetValues } from '../utils/costManagementBudgets'
7+
import { CostManagementView } from './CostManagementView'
8+
9+
const baseBudgetValues: BudgetValues = {
10+
...EMPTY_BUDGET_VALUES,
11+
account: '1',
12+
}
13+
14+
const baseBudgetSimulation: BudgetSimulationResult = {
15+
totalBill: 0.4,
16+
blockedUsers: 1,
17+
blockedRequests: 7,
18+
blockedIncludedCreditsAic: 0,
19+
allowedAicQuantity: 40,
20+
budgetExhausted: true,
21+
firstUserBlockedDate: null,
22+
accountBlockedDate: '2026-06-01',
23+
productBlockedDates: {},
24+
adjustedDailyNetCostByDate: [],
25+
adjustedDailyGrossCostByDate: [],
26+
}
27+
28+
function renderCostManagementView(overrides: Partial<Parameters<typeof CostManagementView>[0]> = {}): string {
29+
return renderToStaticMarkup(createElement(CostManagementView, {
30+
budgetValues: baseBudgetValues,
31+
isIndividualReport: false,
32+
currentPruBill: 0,
33+
currentPruGrossAmount: 0,
34+
currentPruDiscountAmount: 0,
35+
currentPruQuantity: 0,
36+
currentAicBill: 1,
37+
currentAicGrossAmount: 1,
38+
currentAicDiscountAmount: 0,
39+
currentAicQuantity: 100,
40+
includedAicPoolSize: 0,
41+
dailyUsageData: [],
42+
budgetSimulation: null,
43+
budgetSimulationError: null,
44+
isApplyingBudgetSimulation: false,
45+
onBudgetValueChange: vi.fn(),
46+
onApplyBudgetSimulation: vi.fn(),
47+
showOrganizationPromotionalDataDisclaimer: false,
48+
...overrides,
49+
}))
50+
}
51+
52+
describe('CostManagementView', () => {
53+
it('shows budget controls for native usage-based billing reports', () => {
54+
const html = renderCostManagementView({
55+
reportMode: 'native-ai-credits',
56+
})
57+
58+
expect(html).toContain('Set USD budgets and preview how they would affect usage-based billing for this report.')
59+
expect(html).toContain('Account-level budget')
60+
expect(html).toContain('Apply')
61+
expect(html).not.toContain('Budget simulation is not available')
62+
expect(html).not.toContain('native AI Credits reports yet')
63+
expect(html).not.toContain('PRU')
64+
})
65+
66+
it('uses AI Credits result labels instead of PRU labels for native reports', () => {
67+
const html = renderCostManagementView({
68+
reportMode: 'native-ai-credits',
69+
budgetSimulation: baseBudgetSimulation,
70+
})
71+
72+
expect(html).toContain('Blocked AI Credits')
73+
expect(html).toContain('60')
74+
expect(html).toContain('Simulated AI Credits additional usage spend')
75+
expect(html).not.toContain('Blocked PRUs')
76+
expect(html).not.toContain('later requests')
77+
})
78+
79+
it('keeps PRU blocked-usage labeling for transition-period reports', () => {
80+
const html = renderCostManagementView({
81+
reportMode: 'transition-period-billing-preview',
82+
budgetSimulation: baseBudgetSimulation,
83+
})
84+
85+
expect(html).toContain('Blocked PRUs')
86+
expect(html).toContain('later requests')
87+
expect(html).not.toContain('Blocked AI Credits')
88+
})
89+
})

src/views/CostManagementView.tsx

Lines changed: 32 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ type CostManagementViewProps = {
4242
const ACCOUNT_BUDGET_FIELD: { field: BudgetField; label: string; description: string } = {
4343
field: 'account',
4444
label: 'Account-level budget',
45-
description: 'Controls additional spend only for the current billing period.\nDoes not impact included credits.',
45+
description: 'Controls additional AI Credits spend only for the current billing period.\nDoes not impact included credits.',
4646
}
4747

4848
const USER_BUDGET_FIELDS: Array<{ field: BudgetField; label: string; description: string }> = [
4949
{
5050
field: 'user',
5151
label: 'Universal user-level budget',
52-
description: 'Default per-user limit for cumulative AIC gross cost.',
52+
description: 'Default per-user limit for cumulative AI Credits gross cost.',
5353
},
5454
{
5555
field: 'heavyUser',
@@ -67,25 +67,25 @@ const INDIVIDUAL_BUDGET_FIELDS: Array<{ field: BudgetField; label: string; descr
6767
{
6868
field: 'account',
6969
label: 'Additional usage budget',
70-
description: 'Controls additional usage spend only for the current billing period.\nDoes not impact included credits.',
70+
description: 'Controls additional AI Credits usage spend only for the current billing period.\nDoes not impact included credits.',
7171
},
7272
]
7373

7474
const PRODUCT_BUDGET_FIELDS: Array<{ field: BudgetField; label: string; description: string }> = [
7575
{
7676
field: 'productCloudAgent',
7777
label: PRODUCT_BUDGET_COPILOT_CLOUD_AGENT,
78-
description: 'Applies only to additional AIC spend for Copilot Cloud Agent usage.',
78+
description: 'Applies only to additional AI Credits spend for Copilot Cloud Agent usage.',
7979
},
8080
{
8181
field: 'productSpark',
8282
label: PRODUCT_BUDGET_SPARK,
83-
description: 'Applies only to additional AIC spend for Spark usage.',
83+
description: 'Applies only to additional AI Credits spend for Spark usage.',
8484
},
8585
{
8686
field: 'productCopilot',
8787
label: PRODUCT_BUDGET_COPILOT,
88-
description: 'Applies only to additional AIC spend for Copilot usage.',
88+
description: 'Applies only to additional AI Credits spend for Copilot usage.',
8989
},
9090
]
9191

@@ -218,45 +218,21 @@ export function CostManagementView({
218218
currentAicQuantity,
219219
licenseAmount,
220220
])
221-
222-
if (isNativeAiCredits) {
223-
return (
224-
<section className="flex flex-col gap-6" aria-label="Cost management">
225-
<div className="flex flex-col gap-1">
226-
<h2 className="m-0 text-lg text-fg-default">Cost management</h2>
227-
<p className="m-0 text-[13px] text-fg-muted">Budget simulation is not available for usage-based billing reports yet.</p>
228-
</div>
229-
230-
<BillingTotalsCards
231-
pruNetAmount={currentPruBill}
232-
pruGrossAmount={currentPruGrossAmount}
233-
pruDiscountAmount={currentPruDiscountAmount}
234-
pruQuantity={currentPruQuantity}
235-
aicNetAmount={currentAicBill}
236-
aicGrossAmount={currentAicGrossAmount}
237-
aicDiscountAmount={currentAicDiscountAmount}
238-
aicQuantity={currentAicQuantity}
239-
licenseAmount={licenseAmount}
240-
licenseSeatCounts={licenseSeatCounts}
241-
showExistingDiscountDisclaimer={!isIndividualReport}
242-
showPromotionalDataDisclaimer={isIndividualReport}
243-
showOrganizationPromotionalDataDisclaimer={!isIndividualReport && showOrganizationPromotionalDataDisclaimer}
244-
upgradeRecommendation={upgradeRecommendation}
245-
reportMode={reportMode}
246-
/>
247-
248-
<div className="bg-bg-default border border-border-default rounded-md px-5 py-5 text-sm text-fg-muted leading-normal">
249-
Usage-based billing reports already contain AI Credits quantities and costs. Budget controls will be enabled after the simulator can process usage-based billing rows directly.
250-
</div>
251-
</section>
252-
)
253-
}
221+
const blockedUsageValue = budgetSimulation
222+
? isNativeAiCredits
223+
? formatAic(Math.max(currentAicQuantity - budgetSimulation.allowedAicQuantity, 0))
224+
: budgetSimulation.blockedRequests.toLocaleString()
225+
: '0'
254226

255227
return (
256228
<section className="flex flex-col gap-6" aria-label="Cost management">
257229
<div className="flex flex-col gap-1">
258230
<h2 className="m-0 text-lg text-fg-default">Cost management</h2>
259-
<p className="m-0 text-[13px] text-fg-muted">Set USD budgets and preview how they would affect the uploaded report.</p>
231+
<p className="m-0 text-[13px] text-fg-muted">
232+
{isNativeAiCredits
233+
? 'Set USD budgets and preview how they would affect usage-based billing for this report.'
234+
: 'Set USD budgets and preview how they would affect the uploaded report.'}
235+
</p>
260236
</div>
261237

262238
<BillingTotalsCards
@@ -308,10 +284,10 @@ export function CostManagementView({
308284
<div className="flex flex-col gap-1">
309285
<strong className="text-sm font-semibold text-fg-default">User-level budgets</strong>
310286
<p className="m-0 text-[13px] text-fg-muted">
311-
These budgets apply per user to cumulative AIC gross cost. Heavy and Power budgets replace the universal budget for users classified into those groups.
287+
These budgets apply per user to cumulative AI Credits gross cost. Heavy and Power budgets replace the universal budget for users classified into those groups.
312288
</p>
313289
<p className="m-0 text-[13px] text-fg-muted">
314-
Values are prepopulated from the average AIC gross cost for the spending groups identified in the <strong className="text-fg-default">Spend Insights</strong> section.
290+
Values are prepopulated from the average AI Credits gross cost for the spending groups identified in the <strong className="text-fg-default">Spend Insights</strong> section.
315291
</p>
316292
</div>
317293

@@ -348,7 +324,7 @@ export function CostManagementView({
348324
<div className="flex flex-col gap-1">
349325
<strong className="text-sm font-semibold text-fg-default">Product-level budgets</strong>
350326
<p className="m-0 text-[13px] text-fg-muted">
351-
These budgets apply only to <strong className="text-fg-default">AIC additional spend</strong>. Included credits can still be used before additional spend blocking starts.
327+
These budgets apply only to <strong className="text-fg-default">AI Credits additional spend</strong>. Included credits can still be used before additional spend blocking starts.
352328
</p>
353329
</div>
354330

@@ -384,8 +360,8 @@ export function CostManagementView({
384360
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
385361
<p className="m-0 text-[13px] text-fg-muted">
386362
{isIndividualReport
387-
? <>The simulation applies the <strong className="text-fg-default">additional usage budget</strong> against total paid AIC additional spend after included credits are used.</>
388-
: <>The simulation applies <strong className="text-fg-default">User-level budgets</strong> per user to cumulative AIC gross cost, the <strong className="text-fg-default">account-level budget</strong> to total paid AIC additional spend, and <strong className="text-fg-default">product-level budgets</strong> to additional spend for each product bucket. The first limit reached blocks later requests for that scope.</>}
363+
? <>The simulation applies the <strong className="text-fg-default">additional usage budget</strong> against total paid AI Credits additional spend after included credits are used.</>
364+
: <>The simulation applies <strong className="text-fg-default">User-level budgets</strong> per user to cumulative AI Credits gross cost, the <strong className="text-fg-default">account-level budget</strong> to total paid AI Credits additional spend, and <strong className="text-fg-default">product-level budgets</strong> to additional spend for each product bucket. The first limit reached blocks later {isNativeAiCredits ? 'usage' : 'requests'} for that scope.</>}
389365
</p>
390366
<button
391367
type="button"
@@ -415,15 +391,15 @@ export function CostManagementView({
415391
<div key={card.label} className="bg-bg-default border border-border-default rounded-md px-5 py-[28px] text-center">
416392
<div className="text-[13px] font-medium text-fg-muted uppercase tracking-[0.5px] mb-3">{card.label}</div>
417393
<div className="text-4xl font-bold leading-[1.2] text-app-savings-fg">{formatUsd(card.totalAmount)}</div>
418-
<div className="text-sm text-fg-default mt-[6px]">{formatAic(card.aicQuantity)} AICs</div>
419-
<div className="text-xs text-fg-muted mt-1">1 AIC = $0.01</div>
394+
<div className="text-sm text-fg-default mt-[6px]">{formatAic(card.aicQuantity)} AI Credits</div>
395+
<div className="text-xs text-fg-muted mt-1">1 AI Credit = $0.01</div>
420396
<div className="mt-4 pt-3 border-t border-border-default w-full flex flex-col gap-[6px] text-left">
421397
<div className="flex justify-between items-center text-[13px] text-fg-default tabular-nums">
422-
<span>Consumed AICs</span>
398+
<span>Consumed AI Credits</span>
423399
<span>{formatUsd(card.grossAmount)}</span>
424400
</div>
425401
<div className="flex justify-between items-center text-[13px] text-fg-muted tabular-nums">
426-
<span>Included AICs</span>
402+
<span>Included AI Credits</span>
427403
<span>{formatUsd(card.includedAmount)}</span>
428404
</div>
429405
<div className="pt-[6px] border-t border-dotted border-border-muted flex flex-col gap-[6px]">
@@ -452,24 +428,24 @@ export function CostManagementView({
452428
</div>
453429

454430
<p className="m-0 text-center text-[13px] text-fg-muted">
455-
Simulated AIC additional usage spend: <strong className="text-fg-default">{formatUsd(budgetSimulation.totalBill)}</strong> with the configured budgets applied.
431+
Simulated AI Credits additional usage spend: <strong className="text-fg-default">{formatUsd(budgetSimulation.totalBill)}</strong> with the configured budgets applied.
456432
</p>
457433

458434
{cumulativeSimulationSeries && cumulativeSimulationSeries.labels.length > 0 && (
459435
<DualAxisLineChart
460-
title="Cumulative AIC gross cost: current vs simulated"
436+
title="Cumulative AI Credits gross cost: current vs simulated"
461437
labels={cumulativeSimulationSeries.labels}
462438
series={[
463439
{
464-
label: 'Current AIC gross cost',
440+
label: 'Current AI Credits gross cost',
465441
legendOrder: 3,
466442
color: CURRENT_GROSS_COST_COLOR,
467443
data: cumulativeSimulationSeries.current,
468444
yAxisID: 'y',
469445
order: 2,
470446
},
471447
{
472-
label: 'Simulated AIC gross cost',
448+
label: 'Simulated AI Credits gross cost',
473449
legendLabel: 'Simulated - within included pool',
474450
legendOrder: 1,
475451
color: SIMULATED_INCLUDED_COLOR,
@@ -559,12 +535,12 @@ export function CostManagementView({
559535
</tr>
560536
)}
561537
<tr>
562-
<td className={td}>Blocked PRUs</td>
563-
<td className={`${tdNum} font-semibold text-fg-default`}>{budgetSimulation.blockedRequests.toLocaleString()}</td>
538+
<td className={td}>{isNativeAiCredits ? 'Blocked AI Credits' : 'Blocked PRUs'}</td>
539+
<td className={`${tdNum} font-semibold text-fg-default`}>{blockedUsageValue}</td>
564540
</tr>
565541
{!isIndividualReport && (
566542
<tr>
567-
<td className={td}>Included credits blocked by user budgets</td>
543+
<td className={td}>Included AI Credits blocked by user budgets</td>
568544
<td className={`${tdNum} font-semibold text-fg-default`}>{formatAic(budgetSimulation.blockedIncludedCreditsAic)}</td>
569545
</tr>
570546
)}

0 commit comments

Comments
 (0)