Skip to content

Commit 324a343

Browse files
asizikovCopilot
andcommitted
feat: thread report metadata through pipeline
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fdad591 commit 324a343

4 files changed

Lines changed: 42 additions & 11 deletions

File tree

src/App.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
} from './pipeline/aicIncludedCredits'
3535
import { PRODUCT_BUDGET_COPILOT, PRODUCT_BUDGET_COPILOT_CLOUD_AGENT, PRODUCT_BUDGET_SPARK } from './pipeline/productClassification'
3636
import { runPipeline } from './pipeline/runPipeline'
37+
import type { ReportFormatMetadata } from './pipeline/reportAdapters'
3738
import { runBudgetSimulation, type BudgetSimulationResult } from './utils/budgetSimulation'
3839
import { EMPTY_BUDGET_VALUES, getDefaultBudgetValues, getUserSpendSegmentsByUsername, type BudgetField, type BudgetValues } from './utils/costManagementBudgets'
3940
import { calculateIndividualPlanUpgradeRecommendation, getIndividualLicenseMonthlyCost } from './utils/individualPlanUpgrade'
@@ -49,6 +50,7 @@ const ENTERPRISE_LICENSE_MONTHLY_COST = 39
4950
function App() {
5051
const [status, setStatus] = useState<Status>('idle')
5152
const [quickStats, setQuickStats] = useState<QuickStatsResult | null>(null)
53+
const [reportMetadata, setReportMetadata] = useState<ReportFormatMetadata | null>(null)
5254
const [reportContext, setReportContext] = useState<ReportContextResult | null>(null)
5355
const [error, setError] = useState<string | null>(null)
5456
const [fileName, setFileName] = useState<string | null>(null)
@@ -79,6 +81,7 @@ function App() {
7981

8082
const applyProcessedData = useCallback(({
8183
quickStats,
84+
reportMetadata,
8285
reportContext,
8386
dailyUsageData,
8487
modelUsage,
@@ -88,6 +91,7 @@ function App() {
8891
userUsage,
8992
}: {
9093
quickStats: QuickStatsResult
94+
reportMetadata: ReportFormatMetadata
9195
reportContext: ReportContextResult
9296
dailyUsageData: DailyUsageData[]
9397
modelUsage: ModelUsageResult
@@ -97,6 +101,7 @@ function App() {
97101
userUsage: UserUsageResult
98102
}) => {
99103
setQuickStats(quickStats)
104+
setReportMetadata(reportMetadata)
100105
setReportContext(reportContext)
101106
setDailyUsageData(dailyUsageData)
102107
setModelUsage(modelUsage)
@@ -140,6 +145,7 @@ function App() {
140145
...statsAggregator.result(),
141146
lineCount: pipelineResult.reportRowCount,
142147
},
148+
reportMetadata: pipelineResult.reportMetadata,
143149
reportContext: contextAggregator.result(),
144150
dailyUsageData: dailyAggregator.result().dailyData,
145151
modelUsage: modelAggregator.result(),
@@ -168,6 +174,7 @@ function App() {
168174
setStatus(status)
169175
setError(null)
170176
setQuickStats(null)
177+
setReportMetadata(null)
171178
setReportContext(null)
172179
setDailyUsageData([])
173180
setUserUsage(null)
@@ -476,7 +483,7 @@ function App() {
476483
}
477484
}
478485

479-
const hasReport = status === 'done' && fileName !== null
486+
const hasReport = status === 'done' && fileName !== null && reportMetadata !== null
480487
const showSeatConfirmation = hasReport && seatConfirmationPending
481488
const rangeStart = reportContext?.startDate ?? null
482489
const rangeEnd = reportContext?.endDate ?? null

src/pipeline/reportAdapters.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@ const REPORT_ADAPTERS: Record<ReportFormat, UsageReportAdapter> = {
5454
'native-ai-credits': NATIVE_AI_CREDITS_REPORT_ADAPTER,
5555
}
5656

57-
export function validateUsageReportHeader(header: TokenUsageHeader): void {
58-
validateTokenUsageHeader(header)
57+
export function getDefaultSupportedUsageReportAdapter(): UsageReportAdapter {
58+
return TRANSITION_PERIOD_BILLING_PREVIEW_REPORT_ADAPTER
59+
}
60+
61+
export function validateUsageReportHeader(header: TokenUsageHeader): UsageReportAdapter {
62+
const adapter = getDefaultSupportedUsageReportAdapter()
63+
adapter.validateHeader(header)
64+
return adapter
5965
}
6066

6167
export function detectReportFormat(header: TokenUsageHeader, firstRecord: TokenUsageRecord): ReportFormat {

src/pipeline/runPipeline.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ const NATIVE_AI_CREDITS_HEADER = [
4343
'aic_gross_amount',
4444
].join(',')
4545

46+
const TRANSITION_PERIOD_REPORT_METADATA = {
47+
format: 'transition-period-billing-preview',
48+
label: 'Transition Period Billing Preview report',
49+
supported: true,
50+
}
51+
4652
function createCsv(rows: string[][], header = HEADER): File {
4753
const body = [header, ...rows.map((row) => row.join(','))].join('\n')
4854
return new File([body], 'usage.csv', { type: 'text/csv' })
@@ -65,10 +71,11 @@ class CaptureAggregator implements Aggregator<TokenUsageRecord, TokenUsageRecord
6571
}
6672

6773
describe('runPipeline', () => {
68-
it('accepts a valid header-only report', async () => {
74+
it('returns transition-period metadata for a valid header-only report', async () => {
6975
const aggregator = new CaptureAggregator()
7076

7177
await expect(runPipeline(createCsv([]), [aggregator])).resolves.toEqual({
78+
reportMetadata: TRANSITION_PERIOD_REPORT_METADATA,
7279
reportRowCount: 0,
7380
processedRowCount: 0,
7481
})
@@ -135,7 +142,7 @@ describe('runPipeline', () => {
135142
expect(aggregator.result()).toEqual([])
136143
})
137144

138-
it('filters and normalizes known normalization window rows before AIC allocation', async () => {
145+
it('returns transition-period metadata while processing supported reports', async () => {
139146
const file = createCsv([
140147
['2026-04-25', 'mona', 'copilot', 'copilot_premium_request', 'GPT-5', '0', 'requests', '0.04', '0', '0', '0', 'False', '300', '', '', '0', '0'],
141148
['2026-04-25', 'mona', 'copilot', 'copilot_premium_request', 'GPT-5', '10', 'requests', '0.04', '0.40', '0', '0.40', 'False', '0', '', '', '100', '1.00'],
@@ -155,6 +162,7 @@ describe('runPipeline', () => {
155162
}),
156163
])
157164
expect(result).toEqual({
165+
reportMetadata: TRANSITION_PERIOD_REPORT_METADATA,
158166
reportRowCount: 2,
159167
processedRowCount: 1,
160168
})

src/pipeline/runPipeline.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ import {
77
type TokenUsageHeader,
88
type TokenUsageRecord,
99
} from './parser'
10-
import { validateUsageReportFirstRecord, validateUsageReportHeader } from './reportAdapters'
10+
import {
11+
getDefaultSupportedUsageReportAdapter,
12+
validateUsageReportFirstRecord,
13+
validateUsageReportHeader,
14+
type ReportFormatMetadata,
15+
type UsageReportAdapter,
16+
} from './reportAdapters'
1117
import { streamLines, type StreamProgress } from './streamer'
1218

13-
async function validateFileFormat(file: File): Promise<void> {
19+
async function validateFileFormat(file: File): Promise<ReportFormatMetadata> {
1420
let header: TokenUsageHeader | null = null
21+
let selectedAdapter: UsageReportAdapter | null = null
1522

1623
for await (const line of streamLines(file)) {
1724
const trimmed = line.trimEnd()
@@ -21,13 +28,14 @@ async function validateFileFormat(file: File): Promise<void> {
2128

2229
if (!header) {
2330
header = parseTokenUsageHeader(trimmed)
24-
validateUsageReportHeader(header)
31+
selectedAdapter = validateUsageReportHeader(header)
2532
continue
2633
}
2734

28-
validateUsageReportFirstRecord(header, parseTokenUsageRecord(trimmed, header))
29-
return
35+
return validateUsageReportFirstRecord(header, parseTokenUsageRecord(trimmed, header)).metadata
3036
}
37+
38+
return (selectedAdapter ?? getDefaultSupportedUsageReportAdapter()).metadata
3139
}
3240

3341
export interface PipelineProgress {
@@ -45,6 +53,7 @@ export interface PipelineOptions {
4553
}
4654

4755
export interface PipelineResult {
56+
reportMetadata: ReportFormatMetadata
4857
reportRowCount: number
4958
processedRowCount: number
5059
}
@@ -78,7 +87,7 @@ export async function runPipeline(
7887
options?: PipelineOptions,
7988
): Promise<PipelineResult> {
8089
const { includedCreditsOverrides = {}, progressResolution = 500, onProgress } = options ?? {}
81-
await validateFileFormat(file)
90+
const reportMetadata = await validateFileFormat(file)
8291
let lastProgressStage: PipelineProgress['stage'] | null = null
8392
let lastProgressPercent = -1
8493
let lastProgressTimestamp = 0
@@ -179,6 +188,7 @@ export async function runPipeline(
179188
}
180189

181190
return {
191+
reportMetadata,
182192
reportRowCount,
183193
processedRowCount: rowIndex,
184194
}

0 commit comments

Comments
 (0)