@@ -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'
814import { 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
2666export type AicIncludedCreditsOverrides = {
2767 business ?: number
2868 enterprise ?: number
2969}
3070
3171export 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
3575export type LicenseSummaryRow = {
3676 label : string
@@ -46,6 +86,7 @@ export type LicenseSummary = {
4686
4787export interface AicIncludedCreditsProgressOptions {
4888 onProgress ?: ( progress : StreamProgress ) => void
89+ includedCreditsPolicy ?: IncludedCreditsPolicy
4990}
5091
5192type 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+
79144export 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
89170export 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
106189export 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
116197export 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
126204export 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
136212export 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
146219export 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