Skip to content

Commit db9a67c

Browse files
authored
Merge pull request #144 from github/asizikov/individual-plans-classification
Classify native individual plans
2 parents 6b19be7 + e70fb69 commit db9a67c

9 files changed

Lines changed: 445 additions & 90 deletions

src/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,15 @@ function App() {
125125
let orgAggregator!: OrganizationAggregator
126126
let userAggregator!: UserUsageAggregator
127127

128-
const pipelineResult = await runPipeline(file, (reportMetadata) => {
128+
const pipelineResult = await runPipeline(file, (reportMetadata, includedCreditsPolicy) => {
129129
statsAggregator = new QuickStatsAggregator()
130130
contextAggregator = new ReportContextAggregator()
131131
dailyAggregator = new DailyUsageAggregator(reportMetadata)
132132
modelAggregator = new ModelUsageAggregator(reportMetadata)
133133
productAggregator = new ProductUsageAggregator(reportMetadata)
134134
costCenterAggregator = new CostCenterAggregator(reportMetadata)
135135
orgAggregator = new OrganizationAggregator(reportMetadata)
136-
userAggregator = new UserUsageAggregator(reportMetadata)
136+
userAggregator = new UserUsageAggregator(reportMetadata, includedCreditsPolicy)
137137

138138
return [
139139
statsAggregator,
@@ -522,7 +522,7 @@ function App() {
522522
const licenseAmount = reportPlanScope === 'organization'
523523
? organizationLicenseAmount || undefined
524524
: individualUser
525-
? getIndividualLicenseMonthlyCost(individualUser.totalMonthlyQuota)
525+
? getIndividualLicenseMonthlyCost(individualUser.totalMonthlyQuota, includedCreditsPolicy)
526526
: undefined
527527
const licenseSeatCounts = reportPlanScope === 'organization'
528528
? { business: effectiveBusinessSeats, enterprise: effectiveEnterpriseSeats }
@@ -577,6 +577,7 @@ function App() {
577577
? calculateIndividualPlanUpgradeRecommendation({
578578
totalMonthlyQuota: individualUser.totalMonthlyQuota,
579579
currentMonthlyAicAdditionalUsageBillsUsd: monthlyAicAdditionalUsageBills,
580+
includedCreditsPolicy,
580581
})
581582
: null
582583

src/pipeline/aggregators/userUsageAggregator.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,14 @@ export class UserUsageAggregator implements Aggregator<TokenUsageRecord, UserUsa
159159
private readonly reportFormat: ReportFormat
160160
private readonly quotaPolicy: IncludedCreditsPolicy
161161

162-
constructor(reportMetadataOrFormat?: ReportFormat | ReportFormatMetadata) {
162+
constructor(
163+
reportMetadataOrFormat?: ReportFormat | ReportFormatMetadata,
164+
includedCreditsPolicy?: IncludedCreditsPolicy,
165+
) {
163166
this.reportFormat = getAggregatorReportFormat(reportMetadataOrFormat)
164-
this.quotaPolicy = this.reportFormat === 'native-ai-credits'
167+
this.quotaPolicy = includedCreditsPolicy ?? (this.reportFormat === 'native-ai-credits'
165168
? NATIVE_AI_CREDITS_STANDARD_INCLUDED_CREDITS_POLICY
166-
: TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY
169+
: TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY)
167170
}
168171

169172
onHeader(): void {

src/pipeline/aicIncludedCredits.test.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
NATIVE_AI_CREDITS_STANDARD_INCLUDED_CREDITS_POLICY,
2626
NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY,
2727
TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY,
28+
type IncludedCreditsPolicy,
2829
} from './includedCreditsPolicy'
2930
import { CostCenterAggregator } from './aggregators/costCenterAggregator'
3031
import { OrganizationAggregator } from './aggregators/organizationAggregator'
@@ -177,6 +178,23 @@ describe('AIC included credit tiering and pool sizing', () => {
177178
expect(TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.organizationPlans.enterprise.monthlyIncludedCredits).toBe(
178179
ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS,
179180
)
181+
expect(TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.individualPlans['pro-student']?.identity).toEqual({
182+
tier: 'pro-student',
183+
quotaUnit: 'pru',
184+
monthlyQuota: PRO_MONTHLY_QUOTA,
185+
})
186+
expect(TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.individualPlans['pro-student']?.monthlyIncludedCredits).toBe(
187+
PRO_MONTHLY_AIC_INCLUDED_CREDITS,
188+
)
189+
expect(TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.individualPlans['pro-plus']?.identity).toEqual({
190+
tier: 'pro-plus',
191+
quotaUnit: 'pru',
192+
monthlyQuota: PRO_PLUS_MONTHLY_QUOTA,
193+
})
194+
expect(TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.individualPlans['pro-plus']?.monthlyIncludedCredits).toBe(
195+
PRO_PLUS_MONTHLY_AIC_INCLUDED_CREDITS,
196+
)
197+
expect('max' in TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.individualPlans).toBe(false)
180198
})
181199

182200
it('keeps native AI Credits policies available without changing default transition-period tiering', () => {
@@ -222,6 +240,72 @@ describe('AIC included credit tiering and pool sizing', () => {
222240
)
223241
})
224242

243+
it('classifies post-preview individual quota identities by individual scope', () => {
244+
expect(getIndividualPlanTier(1500, 'individual', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
245+
'pro-student',
246+
)
247+
expect(getIndividualPlanTier(7000, 'individual', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
248+
'pro-plus',
249+
)
250+
expect(getIndividualPlanTier(20000, 'individual', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
251+
'max',
252+
)
253+
expect(getPlanLabel(1500, 'individual', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
254+
'Copilot Pro',
255+
)
256+
expect(getPlanLabel(7000, 'individual', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
257+
'Copilot Pro+',
258+
)
259+
expect(getPlanLabel(20000, 'individual', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
260+
'Copilot Max',
261+
)
262+
expect(getIndividualMonthlyAicIncludedCredits(
263+
1500,
264+
'individual',
265+
NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY,
266+
)).toBe(1500)
267+
expect(getIndividualMonthlyAicIncludedCredits(
268+
7000,
269+
'individual',
270+
NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY,
271+
)).toBe(7000)
272+
expect(getIndividualMonthlyAicIncludedCredits(
273+
20000,
274+
'individual',
275+
NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY,
276+
)).toBe(20000)
277+
expect(getIndividualPlanTier(20000, 'individual', NATIVE_AI_CREDITS_STANDARD_INCLUDED_CREDITS_POLICY)).toBe(
278+
'max',
279+
)
280+
expect(getIndividualMonthlyAicIncludedCredits(
281+
20000,
282+
'individual',
283+
NATIVE_AI_CREDITS_STANDARD_INCLUDED_CREDITS_POLICY,
284+
)).toBe(20000)
285+
})
286+
287+
it('does not apply post-preview individual quota identities to transition-period reports', () => {
288+
expect(getIndividualPlanTier(7000)).toBeNull()
289+
expect(getIndividualPlanTier(3900)).toBeNull()
290+
expect(getIndividualPlanTier(20000)).toBeNull()
291+
expect(getPlanLabel(7000, 'individual')).toBe('Unknown (7,000 PRUs/month)')
292+
})
293+
294+
it('keeps native 3900 scope-aware as organization Enterprise instead of individual Pro+', () => {
295+
expect(getIndividualPlanTier(3900, 'individual', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
296+
null,
297+
)
298+
expect(getAicIncludedCreditTier(3900, 'organization', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
299+
'enterprise',
300+
)
301+
expect(getPlanLabel(3900, 'individual', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
302+
'Unknown (3,900 AI Credits/month)',
303+
)
304+
expect(getPlanLabel(3900, 'organization', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
305+
'Copilot Enterprise',
306+
)
307+
})
308+
225309
it('classifies 300 as Pro/Student for an individual report', () => {
226310
expect(getIndividualPlanTier(PRO_MONTHLY_QUOTA)).toBe('pro-student')
227311
})
@@ -267,11 +351,22 @@ describe('AIC included credit tiering and pool sizing', () => {
267351
expect(getPlanLabel(0)).toBe('Unknown')
268352
})
269353

354+
it('formats unknown native individual quotas as AI Credits per month', () => {
355+
expect(getPlanLabel(1234, 'individual', NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(
356+
'Unknown (1,234 AI Credits/month)',
357+
)
358+
expect(getPlanLabel(1234, 'individual', NATIVE_AI_CREDITS_STANDARD_INCLUDED_CREDITS_POLICY)).toBe(
359+
'Unknown (1,234 AI Credits/month)',
360+
)
361+
})
362+
270363
it('selects the maximum known monthly quota while ignoring unknown quota values', () => {
271364
expect(selectKnownMonthlyQuota(0, UNKNOWN_HIGH_MONTHLY_QUOTA)).toBe(0)
272365
expect(selectKnownMonthlyQuota(BUSINESS_MONTHLY_QUOTA, UNKNOWN_HIGH_MONTHLY_QUOTA)).toBe(BUSINESS_MONTHLY_QUOTA)
273366
expect(selectKnownMonthlyQuota(UNKNOWN_HIGH_MONTHLY_QUOTA, ENTERPRISE_MONTHLY_QUOTA)).toBe(ENTERPRISE_MONTHLY_QUOTA)
274367
expect(selectKnownMonthlyQuota(BUSINESS_MONTHLY_QUOTA, ENTERPRISE_MONTHLY_QUOTA)).toBe(ENTERPRISE_MONTHLY_QUOTA)
368+
expect(selectKnownMonthlyQuota(0, 10000)).toBe(0)
369+
expect(selectKnownMonthlyQuota(0, 20000, NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)).toBe(20000)
275370
})
276371

277372
it('does not create an organization pool for a single-user Pro/Student report', async () => {
@@ -464,6 +559,21 @@ describe('AIC included credit tiering and pool sizing', () => {
464559
})
465560
})
466561

562+
it('summarizes a native summer single-user report as an individual plan', () => {
563+
const summary = calculateLicenseSummary(
564+
[{ totalMonthlyQuota: 7000 }],
565+
NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY,
566+
)
567+
568+
expect(summary).toEqual({
569+
rows: [
570+
{ label: 'Copilot Pro+', users: 1, includedAic: 7000 },
571+
],
572+
totalUsers: 1,
573+
totalIncludedAic: 7000,
574+
})
575+
})
576+
467577
it('summarizes a single-user report with organization metadata as a business plan', () => {
468578
const summary = calculateLicenseSummary([
469579
{ totalMonthlyQuota: 300, organizations: ['example-org'], costCenters: ['Cost Center A'] },
@@ -607,6 +717,47 @@ describe('AIC included credit tiering and pool sizing', () => {
607717
totalIncludedAic: 10000,
608718
})
609719
})
720+
721+
it('uses native summer individual plans end-to-end for pipeline allocation and license summaries', async () => {
722+
const file = createNativeCsv([
723+
['2026-08-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '21000', 'ai-credits', '0.01', '210.00', '0', '210.00', '20000', '', '', '21000', '210.00'],
724+
])
725+
const capturedRecords = new CaptureAggregator()
726+
let users!: UserUsageAggregator
727+
let resolvedPolicy!: IncludedCreditsPolicy
728+
729+
await runPipeline(file, (reportMetadata, includedCreditsPolicy) => {
730+
resolvedPolicy = includedCreditsPolicy
731+
users = new UserUsageAggregator(reportMetadata, includedCreditsPolicy)
732+
return [capturedRecords, users]
733+
})
734+
735+
expect(resolvedPolicy).toBe(NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY)
736+
expect(capturedRecords.result()).toEqual([
737+
expect.objectContaining({
738+
total_monthly_quota: 20000,
739+
aic_net_amount: 10,
740+
}),
741+
])
742+
expect(users.result().users).toEqual([
743+
expect.objectContaining({
744+
username: 'mona',
745+
totalMonthlyQuota: 20000,
746+
totals: expect.objectContaining({
747+
aicQuantity: 21000,
748+
aicGrossAmount: 210,
749+
aicNetAmount: 10,
750+
}),
751+
}),
752+
])
753+
expect(calculateLicenseSummary(users.result().users, resolvedPolicy)).toEqual({
754+
rows: [
755+
{ label: 'Copilot Max', users: 1, includedAic: 20000 },
756+
],
757+
totalUsers: 1,
758+
totalIncludedAic: 20000,
759+
})
760+
})
610761
})
611762

612763
describe('pooled AIC allocation and derived AIC discounts', () => {

0 commit comments

Comments
 (0)