diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx index 199f3b27cc..30cce5032f 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx @@ -6,9 +6,15 @@ import { GridColLink } from './GridColLink/GridColLink'; import type { IInstanceStatus } from 'interfaces/instance'; import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; import { + BILLING_INCLUDED_REQUESTS, BILLING_PAYG_DEFAULT_MINIMUM_SEATS, BILLING_PAYG_USER_PRICE, + BILLING_TRAFFIC_BUNDLE_PRICE, } from './BillingPlan'; +import { useTrafficDataEstimation } from 'hooks/useTrafficData'; +import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; +import { useMemo } from 'react'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; const StyledInfoLabel = styled(Typography)(({ theme }) => ({ fontSize: theme.fontSizes.smallBody, @@ -27,6 +33,14 @@ export const BillingDetailsPAYG = ({ instanceStatus, }: IBillingDetailsPAYGProps) => { const { users, loading } = useUsers(); + const { + currentPeriod, + toChartData, + toTrafficUsageSum, + endpointsInfo, + getDayLabels, + calculateOverageCost, + } = useTrafficDataEstimation(); const eligibleUsers = users.filter((user) => user.email); @@ -36,7 +50,27 @@ export const BillingDetailsPAYG = ({ const billableUsers = Math.max(eligibleUsers.length, minSeats); const usersCost = BILLING_PAYG_USER_PRICE * billableUsers; - const totalCost = usersCost; + const includedTraffic = BILLING_INCLUDED_REQUESTS; + const traffic = useInstanceTrafficMetrics(currentPeriod.key); + + const overageCost = useMemo(() => { + if (!includedTraffic) { + return 0; + } + const trafficData = toChartData( + getDayLabels(currentPeriod.dayCount), + traffic, + endpointsInfo, + ); + const totalTraffic = toTrafficUsageSum(trafficData); + return calculateOverageCost( + totalTraffic, + includedTraffic, + BILLING_TRAFFIC_BUNDLE_PRICE, + ); + }, [includedTraffic, traffic, currentPeriod, endpointsInfo]); + + const totalCost = usersCost + overageCost; if (loading) return null; @@ -72,6 +106,36 @@ export const BillingDetailsPAYG = ({ + 0} + show={ + + + + Accrued traffic charges + + + view details + + + + + ${BILLING_TRAFFIC_BUNDLE_PRICE} per 1 + million started above included data + + + + ({ + fontSize: theme.fontSizes.mainHeader, + })} + > + ${overageCost.toFixed(2)} + + + + } + /> diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx index e41bbf20fd..08c8467690 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx @@ -14,6 +14,7 @@ import { BILLING_PLAN_PRICES, BILLING_PRO_DEFAULT_INCLUDED_SEATS, BILLING_PRO_USER_PRICE, + BILLING_TRAFFIC_BUNDLE_PRICE, } from './BillingPlan'; import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; @@ -70,7 +71,11 @@ export const BillingDetailsPro = ({ endpointsInfo, ); const totalTraffic = toTrafficUsageSum(trafficData); - return calculateOverageCost(totalTraffic, includedTraffic); + return calculateOverageCost( + totalTraffic, + includedTraffic, + BILLING_TRAFFIC_BUNDLE_PRICE, + ); }, [includedTraffic, traffic, currentPeriod, endpointsInfo]); const totalCost = planPrice + paidAssignedPrice + overageCost; @@ -146,8 +151,8 @@ export const BillingDetailsPro = ({ - $5 dollar per 1 million started above - included data + ${BILLING_TRAFFIC_BUNDLE_PRICE} per 1 + million started above included data diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx index 79209cadc3..24e5796a3f 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx @@ -18,6 +18,7 @@ export const BILLING_PAYG_DEFAULT_MINIMUM_SEATS = 5; export const BILLING_PRO_USER_PRICE = 15; export const BILLING_PRO_DEFAULT_INCLUDED_SEATS = 5; export const BILLING_INCLUDED_REQUESTS = 53_000_000; +export const BILLING_TRAFFIC_BUNDLE_PRICE = 5; const StyledPlanBox = styled('aside')(({ theme }) => ({ padding: theme.spacing(2.5), diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx index 92176b790e..3c6ab14fb9 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx @@ -32,6 +32,7 @@ import { import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin'; import { formatTickValue } from 'component/common/Chart/formatTickValue'; import { useTrafficLimit } from './hooks/useTrafficLimit'; +import { BILLING_TRAFFIC_BUNDLE_PRICE } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan'; const StyledBox = styled(Box)(({ theme }) => ({ display: 'grid', @@ -214,6 +215,7 @@ export const NetworkTrafficUsage: VFC = () => { const calculatedOverageCost = calculateOverageCost( usage, includedTraffic, + BILLING_TRAFFIC_BUNDLE_PRICE, ); setOverageCost(calculatedOverageCost); @@ -223,6 +225,7 @@ export const NetworkTrafficUsage: VFC = () => { data.datasets, includedTraffic, new Date(), + BILLING_TRAFFIC_BUNDLE_PRICE, ), ); } diff --git a/frontend/src/hooks/useTrafficData.test.ts b/frontend/src/hooks/useTrafficData.test.ts index c20a68bbc2..e204f5d3fa 100644 --- a/frontend/src/hooks/useTrafficData.test.ts +++ b/frontend/src/hooks/useTrafficData.test.ts @@ -101,4 +101,39 @@ describe('traffic overage calculation', () => { // 22_500_000 * 3 * 30 = 2_025_000_000 expect(result).toBe(2_025_000_000); }); + + it('supports custom price and unit size', () => { + const dataUsage = 54_000_000; + const includedTraffic = 53_000_000; + const result = calculateOverageCost( + dataUsage, + includedTraffic, + 10, + 500_000, + ); + expect(result).toBe(20); + }); + + it('estimates based on custom price and unit size', () => { + const testData = testData4Days; + testData[0].data.push(22_500_000); + testData[1].data.push(22_500_000); + testData[2].data.push(22_500_000); + const now = new Date(); + const period = toSelectablePeriod(now); + const testNow = new Date(now.getFullYear(), now.getMonth(), 5); + const result = calculateEstimatedMonthlyCost( + period.key, + testData, + 53_000_000, + testNow, + 10, + 500_000, + ); + // 22_500_000 * 3 * 30 = 2_025_000_000 total usage + // 2_025_000_000 - 53_000_000 = 1_972_000_000 overage + // 1_972_000_000 / 500_000 = 3_944 overage units + // 3_944 * 10 = 39_440 + expect(result).toBe(39_440); + }); }); diff --git a/frontend/src/hooks/useTrafficData.ts b/frontend/src/hooks/useTrafficData.ts index 20aaf30153..f2d5291383 100644 --- a/frontend/src/hooks/useTrafficData.ts +++ b/frontend/src/hooks/useTrafficData.ts @@ -2,8 +2,8 @@ import { useState } from 'react'; import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; import type { ChartDataset } from 'chart.js'; -const TRAFFIC_DATA_UNIT_COST = 5; -const TRAFFIC_DATA_UNIT_SIZE = 1_000_000; +const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5; +const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000; export type SelectablePeriod = { key: string; @@ -39,9 +39,13 @@ const endpointsInfo: Record = { }, }; -const calculateTrafficDataCost = (trafficData: number) => { - const unitCount = Math.ceil(trafficData / TRAFFIC_DATA_UNIT_SIZE); - return unitCount * TRAFFIC_DATA_UNIT_COST; +const calculateTrafficDataCost = ( + trafficData: number, + trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST, + trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE, +) => { + const unitCount = Math.ceil(trafficData / trafficUnitSize); + return unitCount * trafficUnitCost; }; const padMonth = (month: number): string => @@ -167,6 +171,8 @@ const getDayLabels = (dayCount: number): number[] => { export const calculateOverageCost = ( dataUsage: number, includedTraffic: number, + trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST, + trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE, ): number => { if (dataUsage === 0) { return 0; @@ -174,7 +180,9 @@ export const calculateOverageCost = ( const overage = Math.floor((dataUsage - includedTraffic) / 1_000_000) * 1_000_000; - return overage > 0 ? calculateTrafficDataCost(overage) : 0; + return overage > 0 + ? calculateTrafficDataCost(overage, trafficUnitCost, trafficUnitSize) + : 0; }; export const calculateProjectedUsage = ( @@ -203,6 +211,8 @@ export const calculateEstimatedMonthlyCost = ( trafficData: ChartDatasetType[], includedTraffic: number, currentDate: Date, + trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST, + trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE, ) => { if (period !== currentPeriod.key) { return 0; @@ -214,7 +224,12 @@ export const calculateEstimatedMonthlyCost = ( trafficData, currentPeriod.dayCount, ); - return calculateOverageCost(projectedUsage, includedTraffic); + return calculateOverageCost( + projectedUsage, + includedTraffic, + trafficUnitCost, + trafficUnitSize, + ); }; export const useTrafficDataEstimation = () => {