Skip to content

Commit e9e5561

Browse files
authored
Merge pull request #99 from rajbos/main
Add interactive potential cost breakdown for premium requests with co…
2 parents 577f0e5 + 30248a7 commit e9e5561

5 files changed

Lines changed: 449 additions & 1 deletion

File tree

src/App.tsx

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ import {
3535
getLastDateFromData,
3636
getExceededRequestDetails,
3737
getUserExceededRequestSummary,
38-
getUniqueUsersExceedingQuota
38+
getUniqueUsersExceedingQuota,
39+
getTotalRequestsForUsersExceedingQuota,
40+
EXCESS_REQUEST_COST
3941
} from "@/lib/utils";
4042

4143
function App() {
@@ -56,6 +58,7 @@ function App() {
5658
const [exceededDetailsData, setExceededDetailsData] = useState<ExceededRequestDetail[]>([]);
5759
const [selectedDate, setSelectedDate] = useState<string | null>(null);
5860
const [usersExceedingQuota, setUsersExceedingQuota] = useState<number>(0);
61+
const [showPotentialCostDetails, setShowPotentialCostDetails] = useState(false);
5962
const fileInputRef = useRef<HTMLInputElement>(null);
6063

6164
// Recalculate users exceeding quota when plan selection changes
@@ -534,6 +537,16 @@ function App() {
534537
{usersExceedingQuota.toLocaleString()}
535538
</span>
536539
</div>
540+
<div className="flex items-center gap-2">
541+
<span className="text-sm text-muted-foreground">Potential Cost:</span>
542+
<span
543+
className="text-lg font-bold text-orange-600 cursor-pointer hover:text-orange-700 transition-colors"
544+
onClick={() => setShowPotentialCostDetails(true)}
545+
title="Click to see cost breakdown"
546+
>
547+
${(data.reduce((sum, item) => sum + item.requestsUsed, 0) * EXCESS_REQUEST_COST).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
548+
</span>
549+
</div>
537550
{powerUserSummary && (
538551
<UITooltip>
539552
<TooltipTrigger asChild>
@@ -1216,6 +1229,78 @@ function App() {
12161229
</DialogContent>
12171230
</Dialog>
12181231

1232+
{/* Potential Cost Details Dialog */}
1233+
<Dialog open={showPotentialCostDetails} onOpenChange={setShowPotentialCostDetails}>
1234+
<DialogContent className="w-[90vw] max-w-2xl">
1235+
<DialogHeader>
1236+
<DialogTitle>Potential Cost Breakdown</DialogTitle>
1237+
</DialogHeader>
1238+
1239+
<div className="space-y-6">
1240+
{data && (
1241+
<>
1242+
{/* All Premium Requests */}
1243+
<Card className="p-4">
1244+
<h3 className="text-md font-medium mb-3">All Premium Requests</h3>
1245+
<div className="space-y-2">
1246+
<div className="flex justify-between items-center">
1247+
<span className="text-sm text-muted-foreground">Total Requests:</span>
1248+
<span className="font-bold">
1249+
{data.reduce((sum, item) => sum + item.requestsUsed, 0).toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 0})}
1250+
</span>
1251+
</div>
1252+
<div className="flex justify-between items-center">
1253+
<span className="text-sm text-muted-foreground">Cost per Request:</span>
1254+
<span className="font-bold">${EXCESS_REQUEST_COST.toFixed(2)}</span>
1255+
</div>
1256+
<Separator className="my-2" />
1257+
<div className="flex justify-between items-center">
1258+
<span className="text-sm font-medium">Total Hypothetical Cost:</span>
1259+
<span className="font-bold text-orange-600 text-lg">
1260+
${(data.reduce((sum, item) => sum + item.requestsUsed, 0) * EXCESS_REQUEST_COST).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
1261+
</span>
1262+
</div>
1263+
</div>
1264+
</Card>
1265+
1266+
{/* Exceeding Users Only */}
1267+
<Card className="p-4">
1268+
<h3 className="text-md font-medium mb-3">Users Exceeding 300 Request Limit Only</h3>
1269+
<div className="space-y-2">
1270+
<div className="flex justify-between items-center">
1271+
<span className="text-sm text-muted-foreground">Exceeding Requests:</span>
1272+
<span className="font-bold text-red-600">
1273+
{getTotalRequestsForUsersExceedingQuota(data, selectedPlan).toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 0})}
1274+
</span>
1275+
</div>
1276+
<div className="flex justify-between items-center">
1277+
<span className="text-sm text-muted-foreground">Cost per Request:</span>
1278+
<span className="font-bold">${EXCESS_REQUEST_COST.toFixed(2)}</span>
1279+
</div>
1280+
<Separator className="my-2" />
1281+
<div className="flex justify-between items-center">
1282+
<span className="text-sm font-medium">Cost for Exceeding Requests Only:</span>
1283+
<span className="font-bold text-red-600 text-lg">
1284+
${(getTotalRequestsForUsersExceedingQuota(data, selectedPlan) * EXCESS_REQUEST_COST).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
1285+
</span>
1286+
</div>
1287+
</div>
1288+
</Card>
1289+
1290+
{/* Summary */}
1291+
<Card className="p-4 bg-secondary/20">
1292+
<h3 className="text-md font-medium mb-3">Summary</h3>
1293+
<div className="text-sm text-muted-foreground space-y-1">
1294+
<p><strong>All Premium Requests:</strong> Shows the hypothetical cost if all premium requests were charged at ${EXCESS_REQUEST_COST} each, regardless of user quotas.</p>
1295+
<p><strong>Exceeding Users Only:</strong> Shows the cost for all requests made by users who have exceeded the {selectedPlan === COPILOT_PLANS.INDIVIDUAL ? '50' : selectedPlan === COPILOT_PLANS.BUSINESS ? '300' : '1000'} monthly request limit per user.</p>
1296+
</div>
1297+
</Card>
1298+
</>
1299+
)}
1300+
</div>
1301+
</DialogContent>
1302+
</Dialog>
1303+
12191304
<Toaster position="top-right" />
12201305
<DeploymentFooter />
12211306
</div>

src/lib/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,3 +618,26 @@ export function getUniqueUsersExceedingQuota(data: CopilotUsageData[], plan: str
618618

619619
return usersExceedingPlan.length;
620620
}
621+
622+
/**
623+
* Get the total requests made by users who have exceeded their quota limits
624+
* @param data - Array of Copilot usage data
625+
* @param plan - The plan type to check limits against (defaults to BUSINESS)
626+
*/
627+
export function getTotalRequestsForUsersExceedingQuota(data: CopilotUsageData[], plan: string = COPILOT_PLANS.BUSINESS): number {
628+
if (!data.length) return 0;
629+
630+
// Get the plan limit for comparison
631+
const planLimit = PLAN_MONTHLY_LIMITS[plan] || PLAN_MONTHLY_LIMITS[COPILOT_PLANS.BUSINESS];
632+
633+
// Aggregate total requests per user
634+
const userTotals: Record<string, number> = {};
635+
data.forEach(item => {
636+
userTotals[item.user] = (userTotals[item.user] || 0) + item.requestsUsed;
637+
});
638+
639+
// Get users who exceed the plan limit and sum their total requests
640+
const usersExceedingPlan = Object.keys(userTotals).filter(user => userTotals[user] > planLimit);
641+
642+
return usersExceedingPlan.reduce((sum, user) => sum + userTotals[user], 0);
643+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { getTotalRequestsForUsersExceedingQuota, getUniqueUsersExceedingQuota, CopilotUsageData, COPILOT_PLANS } from '@/lib/utils';
3+
4+
describe('Exceeding Users Cost Calculation', () => {
5+
// Test data with users who exceed the 300 Business plan limit
6+
const mockData: CopilotUsageData[] = [
7+
// User A: 350 total requests (exceeds 300 limit)
8+
{
9+
timestamp: new Date('2025-01-01T10:00:00Z'),
10+
user: 'userA@example.com',
11+
model: 'gpt-4o',
12+
requestsUsed: 150,
13+
exceedsQuota: false,
14+
totalMonthlyQuota: '300'
15+
},
16+
{
17+
timestamp: new Date('2025-01-02T10:00:00Z'),
18+
user: 'userA@example.com',
19+
model: 'gpt-4o',
20+
requestsUsed: 200,
21+
exceedsQuota: true, // Only this specific request exceeds quota
22+
totalMonthlyQuota: '300'
23+
},
24+
// User B: 250 total requests (does NOT exceed 300 limit)
25+
{
26+
timestamp: new Date('2025-01-01T11:00:00Z'),
27+
user: 'userB@example.com',
28+
model: 'gpt-4o',
29+
requestsUsed: 100,
30+
exceedsQuota: false,
31+
totalMonthlyQuota: '300'
32+
},
33+
{
34+
timestamp: new Date('2025-01-02T11:00:00Z'),
35+
user: 'userB@example.com',
36+
model: 'gpt-4o',
37+
requestsUsed: 150,
38+
exceedsQuota: false,
39+
totalMonthlyQuota: '300'
40+
},
41+
// User C: 450 total requests (exceeds 300 limit)
42+
{
43+
timestamp: new Date('2025-01-01T12:00:00Z'),
44+
user: 'userC@example.com',
45+
model: 'claude-sonnet-3.5',
46+
requestsUsed: 300,
47+
exceedsQuota: false,
48+
totalMonthlyQuota: '300'
49+
},
50+
{
51+
timestamp: new Date('2025-01-02T12:00:00Z'),
52+
user: 'userC@example.com',
53+
model: 'claude-sonnet-3.5',
54+
requestsUsed: 150,
55+
exceedsQuota: true, // Only this specific request exceeds quota
56+
totalMonthlyQuota: '300'
57+
}
58+
];
59+
60+
it('should correctly count users exceeding quota', () => {
61+
// Only userA (350) and userC (450) exceed the 300 limit
62+
const exceedingUsersCount = getUniqueUsersExceedingQuota(mockData, COPILOT_PLANS.BUSINESS);
63+
expect(exceedingUsersCount).toBe(2);
64+
});
65+
66+
it('should correctly calculate total requests for users exceeding quota', () => {
67+
// Should return ALL requests for users who exceed the quota:
68+
// userA: 150 + 200 = 350 requests
69+
// userC: 300 + 150 = 450 requests
70+
// Total: 350 + 450 = 800 requests
71+
const totalRequests = getTotalRequestsForUsersExceedingQuota(mockData, COPILOT_PLANS.BUSINESS);
72+
expect(totalRequests).toBe(800);
73+
});
74+
75+
it('should NOT include requests from users who do not exceed quota', () => {
76+
// userB has 250 total requests (under 300 limit)
77+
// Their requests should NOT be included
78+
const totalRequests = getTotalRequestsForUsersExceedingQuota(mockData, COPILOT_PLANS.BUSINESS);
79+
80+
// Verify userB's requests are not included
81+
const userBTotal = 100 + 150; // 250 requests
82+
expect(totalRequests).not.toEqual(userBTotal);
83+
expect(totalRequests).toBeGreaterThan(userBTotal);
84+
});
85+
86+
it('should handle different plan limits correctly', () => {
87+
// With Individual plan (50 limit), all users should exceed
88+
const individualPlanExceedingRequests = getTotalRequestsForUsersExceedingQuota(mockData, COPILOT_PLANS.INDIVIDUAL);
89+
const totalAllRequests = mockData.reduce((sum, item) => sum + item.requestsUsed, 0);
90+
expect(individualPlanExceedingRequests).toBe(totalAllRequests); // All 1050 requests
91+
92+
// With Enterprise plan (1000 limit), no users should exceed
93+
const enterprisePlanExceedingRequests = getTotalRequestsForUsersExceedingQuota(mockData, COPILOT_PLANS.ENTERPRISE);
94+
expect(enterprisePlanExceedingRequests).toBe(0);
95+
});
96+
97+
it('should be consistent between count and total functions', () => {
98+
const exceedingUsersCount = getUniqueUsersExceedingQuota(mockData, COPILOT_PLANS.BUSINESS);
99+
const totalRequests = getTotalRequestsForUsersExceedingQuota(mockData, COPILOT_PLANS.BUSINESS);
100+
101+
// If there are exceeding users, there should be requests
102+
if (exceedingUsersCount > 0) {
103+
expect(totalRequests).toBeGreaterThan(0);
104+
} else {
105+
expect(totalRequests).toBe(0);
106+
}
107+
});
108+
109+
it('should demonstrate the difference between old logic and new logic', () => {
110+
// Old logic (incorrect): only count requests with exceedsQuota=true
111+
const oldLogicTotal = mockData
112+
.filter(item => item.exceedsQuota)
113+
.reduce((sum, item) => sum + item.requestsUsed, 0);
114+
115+
// This would be: 200 (userA) + 150 (userC) = 350
116+
expect(oldLogicTotal).toBe(350);
117+
118+
// New logic (correct): count ALL requests for users who exceed the plan limit
119+
const newLogicTotal = getTotalRequestsForUsersExceedingQuota(mockData, COPILOT_PLANS.BUSINESS);
120+
121+
// This should be: 350 (userA) + 450 (userC) = 800
122+
expect(newLogicTotal).toBe(800);
123+
124+
// The new logic should give a higher (more accurate) total
125+
expect(newLogicTotal).toBeGreaterThan(oldLogicTotal);
126+
});
127+
128+
it('should handle edge cases correctly', () => {
129+
// Empty data
130+
expect(getTotalRequestsForUsersExceedingQuota([], COPILOT_PLANS.BUSINESS)).toBe(0);
131+
132+
// Single user at exactly the limit
133+
const exactLimitData: CopilotUsageData[] = [{
134+
timestamp: new Date('2025-01-01T10:00:00Z'),
135+
user: 'exactUser@example.com',
136+
model: 'gpt-4o',
137+
requestsUsed: 300, // Exactly at the 300 limit
138+
exceedsQuota: false,
139+
totalMonthlyQuota: '300'
140+
}];
141+
142+
// User with exactly 300 requests should NOT exceed the limit
143+
expect(getTotalRequestsForUsersExceedingQuota(exactLimitData, COPILOT_PLANS.BUSINESS)).toBe(0);
144+
expect(getUniqueUsersExceedingQuota(exactLimitData, COPILOT_PLANS.BUSINESS)).toBe(0);
145+
});
146+
});

0 commit comments

Comments
 (0)