Skip to content

Commit 08a2b7d

Browse files
asizikovCopilot
andcommitted
refactor: add included credit policy types
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2de810c commit 08a2b7d

3 files changed

Lines changed: 287 additions & 54 deletions

File tree

src/pipeline/aicIncludedCredits.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import {
2020
PRO_PLUS_MONTHLY_QUOTA,
2121
selectKnownMonthlyQuota,
2222
} from './aicIncludedCredits'
23+
import {
24+
NATIVE_AI_CREDITS_STANDARD_INCLUDED_CREDITS_POLICY,
25+
NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY,
26+
TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY,
27+
} from './includedCreditsPolicy'
2328
import { CostCenterAggregator } from './aggregators/costCenterAggregator'
2429
import { OrganizationAggregator } from './aggregators/organizationAggregator'
2530
import { UserUsageAggregator } from './aggregators/userUsageAggregator'
@@ -127,6 +132,52 @@ describe('AIC included credit tiering and pool sizing', () => {
127132
expect(getAicIncludedCreditTier(ENTERPRISE_MONTHLY_QUOTA, 'individual')).toBeNull()
128133
})
129134

135+
it('separates transition-period quota identity from included AI Credits entitlement', () => {
136+
expect(TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.organizationPlans.business.identity).toEqual({
137+
tier: 'business',
138+
quotaUnit: 'pru',
139+
monthlyQuota: BUSINESS_MONTHLY_QUOTA,
140+
})
141+
expect(TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.organizationPlans.business.monthlyIncludedCredits).toBe(
142+
BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS,
143+
)
144+
expect(TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.organizationPlans.enterprise.identity).toEqual({
145+
tier: 'enterprise',
146+
quotaUnit: 'pru',
147+
monthlyQuota: ENTERPRISE_MONTHLY_QUOTA,
148+
})
149+
expect(TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.organizationPlans.enterprise.monthlyIncludedCredits).toBe(
150+
ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS,
151+
)
152+
})
153+
154+
it('keeps native AI Credits policies available without changing default transition-period tiering', () => {
155+
expect(NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY.organizationPlans.business.identity).toEqual({
156+
tier: 'business',
157+
quotaUnit: 'aic',
158+
monthlyQuota: 1900,
159+
})
160+
expect(NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY.organizationPlans.business.monthlyIncludedCredits).toBe(
161+
3000,
162+
)
163+
expect(NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY.organizationPlans.enterprise.identity).toEqual({
164+
tier: 'enterprise',
165+
quotaUnit: 'aic',
166+
monthlyQuota: 3900,
167+
})
168+
expect(NATIVE_AI_CREDITS_SUMMER_PROMO_INCLUDED_CREDITS_POLICY.organizationPlans.enterprise.monthlyIncludedCredits).toBe(
169+
7000,
170+
)
171+
expect(NATIVE_AI_CREDITS_STANDARD_INCLUDED_CREDITS_POLICY.organizationPlans.business.monthlyIncludedCredits).toBe(
172+
1900,
173+
)
174+
expect(NATIVE_AI_CREDITS_STANDARD_INCLUDED_CREDITS_POLICY.organizationPlans.enterprise.monthlyIncludedCredits).toBe(
175+
3900,
176+
)
177+
expect(getAicIncludedCreditTier(1900)).toBeNull()
178+
expect(getMonthlyAicIncludedCredits(3900)).toBe(0)
179+
})
180+
130181
it('classifies 300 as Pro/Student for an individual report', () => {
131182
expect(getIndividualPlanTier(PRO_MONTHLY_QUOTA)).toBe('pro-student')
132183
})

src/pipeline/aicIncludedCredits.ts

Lines changed: 133 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,72 @@ import {
55
type TokenUsageHeader,
66
type TokenUsageRecord,
77
} from './parser'
8+
import {
9+
TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY,
10+
type IncludedCreditsPolicy,
11+
type OrganizationIncludedCreditTier,
12+
type PlanIdentity,
13+
} from './includedCreditsPolicy'
814
import { streamLines, type StreamProgress } from './streamer'
915

10-
export const BUSINESS_MONTHLY_QUOTA = 300
11-
export const ENTERPRISE_MONTHLY_QUOTA = 1000
12-
export const PRO_MONTHLY_QUOTA = 300
13-
export const PRO_PLUS_MONTHLY_QUOTA = 1500
14-
const KNOWN_MONTHLY_QUOTAS = new Set([
15-
BUSINESS_MONTHLY_QUOTA,
16-
ENTERPRISE_MONTHLY_QUOTA,
16+
type IndividualPlan = 'pro-student' | 'pro-plus'
17+
18+
type IndividualIncludedCreditsPlans = {
19+
readonly [Tier in IndividualPlan]: {
20+
readonly identity: PlanIdentity<Tier>
21+
readonly label: string
22+
readonly monthlyIncludedCredits: number
23+
}
24+
}
25+
26+
const INDIVIDUAL_INCLUDED_CREDIT_PLANS = {
27+
'pro-student': {
28+
identity: {
29+
tier: 'pro-student',
30+
quotaUnit: 'pru',
31+
monthlyQuota: 300,
32+
},
33+
label: 'Copilot Pro/Student',
34+
monthlyIncludedCredits: 1500,
35+
},
36+
'pro-plus': {
37+
identity: {
38+
tier: 'pro-plus',
39+
quotaUnit: 'pru',
40+
monthlyQuota: 1500,
41+
},
42+
label: 'Copilot Pro+',
43+
monthlyIncludedCredits: 7000,
44+
},
45+
} as const satisfies IndividualIncludedCreditsPlans
46+
47+
export const BUSINESS_MONTHLY_QUOTA = TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.organizationPlans.business.identity.monthlyQuota
48+
export const ENTERPRISE_MONTHLY_QUOTA = TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.organizationPlans.enterprise.identity.monthlyQuota
49+
export const PRO_MONTHLY_QUOTA = INDIVIDUAL_INCLUDED_CREDIT_PLANS['pro-student'].identity.monthlyQuota
50+
export const PRO_PLUS_MONTHLY_QUOTA = INDIVIDUAL_INCLUDED_CREDIT_PLANS['pro-plus'].identity.monthlyQuota
51+
const INDIVIDUAL_KNOWN_MONTHLY_QUOTAS = new Set<number>([
1752
PRO_MONTHLY_QUOTA,
1853
PRO_PLUS_MONTHLY_QUOTA,
1954
])
55+
const TRANSITION_PERIOD_KNOWN_MONTHLY_QUOTAS = new Set<number>([
56+
BUSINESS_MONTHLY_QUOTA,
57+
ENTERPRISE_MONTHLY_QUOTA,
58+
...INDIVIDUAL_KNOWN_MONTHLY_QUOTAS,
59+
])
2060

21-
export const BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS = 3000
22-
export const ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS = 7000
23-
export const PRO_MONTHLY_AIC_INCLUDED_CREDITS = 1500
24-
export const PRO_PLUS_MONTHLY_AIC_INCLUDED_CREDITS = 7000
61+
export const BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS = TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.organizationPlans.business.monthlyIncludedCredits
62+
export const ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS = TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY.organizationPlans.enterprise.monthlyIncludedCredits
63+
export const PRO_MONTHLY_AIC_INCLUDED_CREDITS = INDIVIDUAL_INCLUDED_CREDIT_PLANS['pro-student'].monthlyIncludedCredits
64+
export const PRO_PLUS_MONTHLY_AIC_INCLUDED_CREDITS = INDIVIDUAL_INCLUDED_CREDIT_PLANS['pro-plus'].monthlyIncludedCredits
2565

2666
export type AicIncludedCreditsOverrides = {
2767
business?: number
2868
enterprise?: number
2969
}
3070

3171
export type ReportPlanScope = 'individual' | 'organization'
32-
export type AicIncludedCreditTier = 'business' | 'enterprise' | null
33-
export type IndividualPlanTier = 'pro-student' | 'pro-plus' | null
72+
export type AicIncludedCreditTier = OrganizationIncludedCreditTier | null
73+
export type IndividualPlanTier = IndividualPlan | null
3474

3575
export type LicenseSummaryRow = {
3676
label: string
@@ -46,6 +86,7 @@ export type LicenseSummary = {
4686

4787
export interface AicIncludedCreditsProgressOptions {
4888
onProgress?: (progress: StreamProgress) => void
89+
includedCreditsPolicy?: IncludedCreditsPolicy
4990
}
5091

5192
type ReportScopeUser = {
@@ -64,40 +105,82 @@ function normalizeSeatCount(value: number | undefined): number | null {
64105
return Math.max(0, Math.floor(value))
65106
}
66107

67-
function calculateOrganizationIncludedCreditsPool(overrides: AicIncludedCreditsOverrides): number | null {
108+
function calculateOrganizationIncludedCreditsPool(
109+
overrides: AicIncludedCreditsOverrides,
110+
policy: IncludedCreditsPolicy,
111+
): number | null {
68112
const businessSeats = normalizeSeatCount(overrides.business)
69113
const enterpriseSeats = normalizeSeatCount(overrides.enterprise)
70114

71115
if (businessSeats === null && enterpriseSeats === null) return null
72116

73117
return (
74-
(businessSeats ?? 0) * BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS
75-
+ (enterpriseSeats ?? 0) * ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS
118+
(businessSeats ?? 0) * policy.organizationPlans.business.monthlyIncludedCredits
119+
+ (enterpriseSeats ?? 0) * policy.organizationPlans.enterprise.monthlyIncludedCredits
76120
)
77121
}
78122

123+
function findOrganizationIncludedCreditsPlan(
124+
totalMonthlyQuota: number,
125+
reportPlanScope: ReportPlanScope,
126+
policy: IncludedCreditsPolicy,
127+
) {
128+
if (reportPlanScope !== 'organization') return null
129+
130+
return Object.values(policy.organizationPlans)
131+
.find((plan) => plan.identity.monthlyQuota === totalMonthlyQuota) ?? null
132+
}
133+
134+
function findIndividualIncludedCreditsPlan(
135+
totalMonthlyQuota: number,
136+
reportPlanScope: ReportPlanScope,
137+
) {
138+
if (reportPlanScope !== 'individual') return null
139+
140+
return Object.values(INDIVIDUAL_INCLUDED_CREDIT_PLANS)
141+
.find((plan) => plan.identity.monthlyQuota === totalMonthlyQuota) ?? null
142+
}
143+
79144
export function isKnownMonthlyQuota(totalMonthlyQuota: number): boolean {
80-
return Number.isFinite(totalMonthlyQuota) && KNOWN_MONTHLY_QUOTAS.has(totalMonthlyQuota)
145+
return isKnownMonthlyQuotaForPolicy(totalMonthlyQuota, TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY)
81146
}
82147

83-
export function selectKnownMonthlyQuota(currentQuota: number, candidateQuota: number): number {
84-
const currentKnownQuota = isKnownMonthlyQuota(currentQuota) ? currentQuota : 0
85-
if (!isKnownMonthlyQuota(candidateQuota)) return currentKnownQuota
148+
function isKnownMonthlyQuotaForPolicy(totalMonthlyQuota: number, policy: IncludedCreditsPolicy): boolean {
149+
if (!Number.isFinite(totalMonthlyQuota)) return false
150+
if (policy === TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY) {
151+
return TRANSITION_PERIOD_KNOWN_MONTHLY_QUOTAS.has(totalMonthlyQuota)
152+
}
153+
154+
return (
155+
INDIVIDUAL_KNOWN_MONTHLY_QUOTAS.has(totalMonthlyQuota)
156+
|| Object.values(policy.organizationPlans).some((plan) => plan.identity.monthlyQuota === totalMonthlyQuota)
157+
)
158+
}
159+
160+
export function selectKnownMonthlyQuota(
161+
currentQuota: number,
162+
candidateQuota: number,
163+
policy: IncludedCreditsPolicy = TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY,
164+
): number {
165+
const currentKnownQuota = isKnownMonthlyQuotaForPolicy(currentQuota, policy) ? currentQuota : 0
166+
if (!isKnownMonthlyQuotaForPolicy(candidateQuota, policy)) return currentKnownQuota
86167
return Math.max(currentKnownQuota, candidateQuota)
87168
}
88169

89170
export function inferReportPlanScope(userCount: number, hasOrganizationContext = false): ReportPlanScope {
90171
return userCount === 1 && !hasOrganizationContext ? 'individual' : 'organization'
91172
}
92173

93-
export function getPlanLabel(totalMonthlyQuota: number, reportPlanScope: ReportPlanScope = 'organization'): string {
94-
const organizationTier = getAicIncludedCreditTier(totalMonthlyQuota, reportPlanScope)
95-
if (organizationTier === 'business') return 'Copilot Business'
96-
if (organizationTier === 'enterprise') return 'Copilot Enterprise'
174+
export function getPlanLabel(
175+
totalMonthlyQuota: number,
176+
reportPlanScope: ReportPlanScope = 'organization',
177+
policy: IncludedCreditsPolicy = TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY,
178+
): string {
179+
const organizationPlan = findOrganizationIncludedCreditsPlan(totalMonthlyQuota, reportPlanScope, policy)
180+
if (organizationPlan) return organizationPlan.label
97181

98-
const individualTier = getIndividualPlanTier(totalMonthlyQuota, reportPlanScope)
99-
if (individualTier === 'pro-student') return 'Copilot Pro/Student'
100-
if (individualTier === 'pro-plus') return 'Copilot Pro+'
182+
const individualPlan = findIndividualIncludedCreditsPlan(totalMonthlyQuota, reportPlanScope)
183+
if (individualPlan) return individualPlan.label
101184

102185
if (totalMonthlyQuota > 0) return `Unknown (${totalMonthlyQuota.toLocaleString()} PRUs/month)`
103186
return 'Unknown'
@@ -106,45 +189,36 @@ export function getPlanLabel(totalMonthlyQuota: number, reportPlanScope: ReportP
106189
export function getAicIncludedCreditTier(
107190
totalMonthlyQuota: number,
108191
reportPlanScope: ReportPlanScope = 'organization',
192+
policy: IncludedCreditsPolicy = TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY,
109193
): AicIncludedCreditTier {
110-
if (reportPlanScope !== 'organization') return null
111-
if (totalMonthlyQuota === ENTERPRISE_MONTHLY_QUOTA) return 'enterprise'
112-
if (totalMonthlyQuota === BUSINESS_MONTHLY_QUOTA) return 'business'
113-
return null
194+
return findOrganizationIncludedCreditsPlan(totalMonthlyQuota, reportPlanScope, policy)?.identity.tier ?? null
114195
}
115196

116197
export function getIndividualPlanTier(
117198
totalMonthlyQuota: number,
118199
reportPlanScope: ReportPlanScope = 'individual',
119200
): IndividualPlanTier {
120-
if (reportPlanScope !== 'individual') return null
121-
if (totalMonthlyQuota === PRO_PLUS_MONTHLY_QUOTA) return 'pro-plus'
122-
if (totalMonthlyQuota === PRO_MONTHLY_QUOTA) return 'pro-student'
123-
return null
201+
return findIndividualIncludedCreditsPlan(totalMonthlyQuota, reportPlanScope)?.identity.tier ?? null
124202
}
125203

126204
export function getMonthlyAicIncludedCredits(
127205
totalMonthlyQuota: number,
128206
reportPlanScope: ReportPlanScope = 'organization',
207+
policy: IncludedCreditsPolicy = TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY,
129208
): number {
130-
const tier = getAicIncludedCreditTier(totalMonthlyQuota, reportPlanScope)
131-
if (tier === 'enterprise') return ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS
132-
if (tier === 'business') return BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS
133-
return 0
209+
return findOrganizationIncludedCreditsPlan(totalMonthlyQuota, reportPlanScope, policy)?.monthlyIncludedCredits ?? 0
134210
}
135211

136212
export function getIndividualMonthlyAicIncludedCredits(
137213
totalMonthlyQuota: number,
138214
reportPlanScope: ReportPlanScope = 'individual',
139215
): number {
140-
const tier = getIndividualPlanTier(totalMonthlyQuota, reportPlanScope)
141-
if (tier === 'pro-plus') return PRO_PLUS_MONTHLY_AIC_INCLUDED_CREDITS
142-
if (tier === 'pro-student') return PRO_MONTHLY_AIC_INCLUDED_CREDITS
143-
return 0
216+
return findIndividualIncludedCreditsPlan(totalMonthlyQuota, reportPlanScope)?.monthlyIncludedCredits ?? 0
144217
}
145218

146219
export function calculateLicenseSummary(
147220
users: Array<{ totalMonthlyQuota: number } & ReportScopeUser>,
221+
policy: IncludedCreditsPolicy = TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY,
148222
): LicenseSummary {
149223
const reportPlanScope = inferReportPlanScope(users.length, hasOrganizationContext(users))
150224
if (reportPlanScope === 'individual') {
@@ -161,22 +235,22 @@ export function calculateLicenseSummary(
161235
}
162236

163237
const rows: LicenseSummaryRow[] = [
164-
{ label: 'Copilot Business', users: 0, includedAic: 0 },
165-
{ label: 'Copilot Enterprise', users: 0, includedAic: 0 },
238+
{ label: policy.organizationPlans.business.label, users: 0, includedAic: 0 },
239+
{ label: policy.organizationPlans.enterprise.label, users: 0, includedAic: 0 },
166240
]
167241

168242
users.forEach((user) => {
169-
const tier = getAicIncludedCreditTier(user.totalMonthlyQuota, reportPlanScope)
170-
const includedAic = getMonthlyAicIncludedCredits(user.totalMonthlyQuota, reportPlanScope)
243+
const plan = findOrganizationIncludedCreditsPlan(user.totalMonthlyQuota, reportPlanScope, policy)
244+
if (!plan) return
171245

172-
if (tier === 'business') {
246+
if (plan.identity.tier === 'business') {
173247
rows[0].users += 1
174-
rows[0].includedAic += includedAic
248+
rows[0].includedAic += plan.monthlyIncludedCredits
175249
}
176250

177-
if (tier === 'enterprise') {
251+
if (plan.identity.tier === 'enterprise') {
178252
rows[1].users += 1
179-
rows[1].includedAic += includedAic
253+
rows[1].includedAic += plan.monthlyIncludedCredits
180254
}
181255
})
182256

@@ -192,6 +266,7 @@ export async function calculateAicIncludedCreditsContext(
192266
overrides: AicIncludedCreditsOverrides = {},
193267
options?: AicIncludedCreditsProgressOptions,
194268
): Promise<AicIncludedCreditsContext> {
269+
const includedCreditsPolicy = options?.includedCreditsPolicy ?? TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY
195270
let header: TokenUsageHeader | null = null
196271
const quotasByUser = new Map<string, number>()
197272
let hasOrganizationContext = false
@@ -216,7 +291,11 @@ export async function calculateAicIncludedCreditsContext(
216291
}
217292

218293
const currentQuota = quotasByUser.get(username) ?? 0
219-
quotasByUser.set(username, selectKnownMonthlyQuota(currentQuota, record.total_monthly_quota))
294+
quotasByUser.set(username, selectKnownMonthlyQuota(
295+
currentQuota,
296+
record.total_monthly_quota,
297+
includedCreditsPolicy,
298+
))
220299
}
221300

222301
const reportPlanScope = inferReportPlanScope(quotasByUser.size, hasOrganizationContext)
@@ -229,12 +308,12 @@ export async function calculateAicIncludedCreditsContext(
229308
}
230309
}
231310

232-
const overriddenOrganizationIncludedCreditPool = calculateOrganizationIncludedCreditsPool(overrides)
311+
const overriddenOrganizationIncludedCreditPool = calculateOrganizationIncludedCreditsPool(overrides, includedCreditsPolicy)
233312

234313
return {
235314
reportPlanScope,
236315
organizationIncludedCreditsPool: overriddenOrganizationIncludedCreditPool ?? Array.from(quotasByUser.values()).reduce(
237-
(total, quota) => total + getMonthlyAicIncludedCredits(quota, reportPlanScope),
316+
(total, quota) => total + getMonthlyAicIncludedCredits(quota, reportPlanScope, includedCreditsPolicy),
238317
0,
239318
),
240319
individualMonthlyIncludedCredits: 0,

0 commit comments

Comments
 (0)