From 740a1d4343d7e01e0648ea8cefb14adcd611df55 Mon Sep 17 00:00:00 2001 From: Anton Sizikov Date: Fri, 22 May 2026 14:32:29 +0200 Subject: [PATCH] fix: ignore unknown quota values in plan detection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../aggregators/userUsageAggregator.ts | 7 +- src/pipeline/aicIncludedCredits.test.ts | 69 +++++++++++++++++++ src/pipeline/aicIncludedCredits.ts | 20 +++++- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/pipeline/aggregators/userUsageAggregator.ts b/src/pipeline/aggregators/userUsageAggregator.ts index d3d8543..3a18b1c 100644 --- a/src/pipeline/aggregators/userUsageAggregator.ts +++ b/src/pipeline/aggregators/userUsageAggregator.ts @@ -3,6 +3,7 @@ import { getUsageMetrics, type TokenUsageHeader, type TokenUsageRecord } from '. import { getDisplayModelName } from '../modelLabels' import { getFriendlyProductName } from '../productClassification' import { classifyUserSpendSegments, type UserSpendSegmentId } from '../../utils/userSpendSegments' +import { selectKnownMonthlyQuota } from '../aicIncludedCredits' export type UserModelDailyUsage = { requests: number @@ -167,7 +168,7 @@ export class UserUsageAggregator implements Aggregator user.totalMonthlyQuota) { - user.totalMonthlyQuota = record.total_monthly_quota - } + user.totalMonthlyQuota = selectKnownMonthlyQuota(user.totalMonthlyQuota, record.total_monthly_quota) const organization = record.organization.trim() if (organization) { diff --git a/src/pipeline/aicIncludedCredits.test.ts b/src/pipeline/aicIncludedCredits.test.ts index d17611e..a8b3cd0 100644 --- a/src/pipeline/aicIncludedCredits.test.ts +++ b/src/pipeline/aicIncludedCredits.test.ts @@ -18,6 +18,7 @@ import { PRO_PLUS_MONTHLY_AIC_INCLUDED_CREDITS, PooledAicIncludedCreditsAllocator, PRO_PLUS_MONTHLY_QUOTA, + selectKnownMonthlyQuota, } from './aicIncludedCredits' import { CostCenterAggregator } from './aggregators/costCenterAggregator' import { OrganizationAggregator } from './aggregators/organizationAggregator' @@ -45,6 +46,7 @@ const HEADER = [ 'aic_quantity', 'aic_gross_amount', ].join(',') +const UNKNOWN_HIGH_MONTHLY_QUOTA = 2147483647 function createCsv(rows: string[][]): File { const body = [HEADER, ...rows.map((row) => row.join(','))].join('\n') @@ -170,6 +172,13 @@ describe('AIC included credit tiering and pool sizing', () => { expect(getPlanLabel(0)).toBe('Unknown') }) + it('selects the maximum known monthly quota while ignoring unknown quota values', () => { + expect(selectKnownMonthlyQuota(0, UNKNOWN_HIGH_MONTHLY_QUOTA)).toBe(0) + expect(selectKnownMonthlyQuota(BUSINESS_MONTHLY_QUOTA, UNKNOWN_HIGH_MONTHLY_QUOTA)).toBe(BUSINESS_MONTHLY_QUOTA) + expect(selectKnownMonthlyQuota(UNKNOWN_HIGH_MONTHLY_QUOTA, ENTERPRISE_MONTHLY_QUOTA)).toBe(ENTERPRISE_MONTHLY_QUOTA) + expect(selectKnownMonthlyQuota(BUSINESS_MONTHLY_QUOTA, ENTERPRISE_MONTHLY_QUOTA)).toBe(ENTERPRISE_MONTHLY_QUOTA) + }) + it('does not create an organization pool for a single-user Pro/Student report', async () => { const file = createCsv([ ['2026-03-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', '300', '', '', '10', '0.10'], @@ -213,6 +222,32 @@ describe('AIC included credit tiering and pool sizing', () => { ) }) + it('ignores unknown high quota rows when sizing a business pool', async () => { + const file = createCsv([ + ['2026-03-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', '300', 'octo', 'Cats', '10', '0.10'], + ['2026-03-02', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'octo', 'Cats', '10', '0.10'], + ]) + + await expect(calculateAicIncludedCreditsPool(file)).resolves.toBe(BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS) + }) + + it('ignores unknown high quota rows when sizing an enterprise pool', async () => { + const file = createCsv([ + ['2026-03-01', 'hubot', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', '1000', 'octo', 'Cats', '10', '0.10'], + ['2026-03-02', 'hubot', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'octo', 'Cats', '10', '0.10'], + ]) + + await expect(calculateAicIncludedCreditsPool(file)).resolves.toBe(ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS) + }) + + it('does not size a pool from users that only have unknown high quota rows', async () => { + const file = createCsv([ + ['2026-03-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'octo', 'Cats', '10', '0.10'], + ]) + + await expect(calculateAicIncludedCreditsPool(file)).resolves.toBe(0) + }) + it('uses override seat counts instead of active users when sizing an organization pool', async () => { const file = createCsv([ ['2026-03-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', '300', 'example-org', 'Cost Center A', '10', '0.10'], @@ -406,6 +441,40 @@ describe('AIC included credit tiering and pool sizing', () => { }), ]) }) + + it('keeps user aggregation and license summary aligned when unknown high quota rows are present', async () => { + const file = createCsv([ + ['2026-03-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '1500', 'ai-credits', '0.01', '15.00', '0', '15.00', 'False', '300', 'example-org', 'Cost Center A', '1500', '15.00'], + ['2026-03-02', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '1500', 'ai-credits', '0.01', '15.00', '0', '15.00', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'example-org', 'Cost Center A', '1500', '15.00'], + ['2026-03-03', 'hubot', 'copilot', 'copilot_ai_credit', 'GPT-5', '3500', 'ai-credits', '0.01', '35.00', '0', '35.00', 'False', '1000', 'example-org', 'Cost Center A', '3500', '35.00'], + ['2026-03-04', 'hubot', 'copilot', 'copilot_ai_credit', 'GPT-5', '3500', 'ai-credits', '0.01', '35.00', '0', '35.00', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'example-org', 'Cost Center A', '3500', '35.00'], + ['2026-03-05', 'octocat', 'copilot', 'copilot_ai_credit', 'GPT-5', '100', 'ai-credits', '0.01', '1.00', '0', '1.00', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'example-org', 'Cost Center B', '100', '1.00'], + ]) + const users = new UserUsageAggregator() + + await runPipeline(file, [users]) + + const userResult = users.result().users + const licenseSummary = calculateLicenseSummary(userResult) + + expect(userResult.find((user) => user.username === 'mona')).toEqual(expect.objectContaining({ + totalMonthlyQuota: BUSINESS_MONTHLY_QUOTA, + })) + expect(userResult.find((user) => user.username === 'hubot')).toEqual(expect.objectContaining({ + totalMonthlyQuota: ENTERPRISE_MONTHLY_QUOTA, + })) + expect(userResult.find((user) => user.username === 'octocat')).toEqual(expect.objectContaining({ + totalMonthlyQuota: 0, + })) + expect(licenseSummary).toEqual({ + rows: [ + { label: 'Copilot Business', users: 1, includedAic: 3000 }, + { label: 'Copilot Enterprise', users: 1, includedAic: 7000 }, + ], + totalUsers: 2, + totalIncludedAic: 10000, + }) + }) }) describe('pooled AIC allocation and derived AIC discounts', () => { diff --git a/src/pipeline/aicIncludedCredits.ts b/src/pipeline/aicIncludedCredits.ts index 65c784e..bcc2681 100644 --- a/src/pipeline/aicIncludedCredits.ts +++ b/src/pipeline/aicIncludedCredits.ts @@ -11,6 +11,12 @@ export const BUSINESS_MONTHLY_QUOTA = 300 export const ENTERPRISE_MONTHLY_QUOTA = 1000 export const PRO_MONTHLY_QUOTA = 300 export const PRO_PLUS_MONTHLY_QUOTA = 1500 +const KNOWN_MONTHLY_QUOTAS = new Set([ + BUSINESS_MONTHLY_QUOTA, + ENTERPRISE_MONTHLY_QUOTA, + PRO_MONTHLY_QUOTA, + PRO_PLUS_MONTHLY_QUOTA, +]) export const BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS = 3000 export const ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS = 7000 @@ -70,6 +76,16 @@ function calculateOrganizationIncludedCreditsPool(overrides: AicIncludedCreditsO ) } +export function isKnownMonthlyQuota(totalMonthlyQuota: number): boolean { + return Number.isFinite(totalMonthlyQuota) && KNOWN_MONTHLY_QUOTAS.has(totalMonthlyQuota) +} + +export function selectKnownMonthlyQuota(currentQuota: number, candidateQuota: number): number { + const currentKnownQuota = isKnownMonthlyQuota(currentQuota) ? currentQuota : 0 + if (!isKnownMonthlyQuota(candidateQuota)) return currentKnownQuota + return Math.max(currentKnownQuota, candidateQuota) +} + export function inferReportPlanScope(userCount: number, hasOrganizationContext = false): ReportPlanScope { return userCount === 1 && !hasOrganizationContext ? 'individual' : 'organization' } @@ -200,9 +216,7 @@ export async function calculateAicIncludedCreditsContext( } const currentQuota = quotasByUser.get(username) ?? 0 - if (record.total_monthly_quota > currentQuota) { - quotasByUser.set(username, record.total_monthly_quota) - } + quotasByUser.set(username, selectKnownMonthlyQuota(currentQuota, record.total_monthly_quota)) } const reportPlanScope = inferReportPlanScope(quotasByUser.size, hasOrganizationContext)