1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

chore: PAYG traffic bundles (#8805)

https://linear.app/unleash/issue/2-2989/unleash-payg-auto-traffic-billing

Integrates auto traffic bundle billing with PAYG.

Currently assumes the PAYG traffic bundle will have the same
`$5/1_000_000` cost as the existing Pro traffic bundle, with the same
`53_000_000` included requests. However some adjustments are included so
it's easier to change this in the future.
This commit is contained in:
Nuno Góis 2024-11-20 15:20:50 +00:00 committed by GitHub
parent 332440491a
commit b7af9b7ec3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 134 additions and 11 deletions

View File

@ -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 = ({
</Typography>
</GridCol>
</GridRow>
<ConditionallyRender
condition={overageCost > 0}
show={
<GridRow>
<GridCol vertical>
<Typography>
<strong>Accrued traffic charges</strong>
<GridColLink>
<Link to='/admin/network/data-usage'>
view details
</Link>
</GridColLink>
</Typography>
<StyledInfoLabel>
${BILLING_TRAFFIC_BUNDLE_PRICE} per 1
million started above included data
</StyledInfoLabel>
</GridCol>
<GridCol>
<Typography
sx={(theme) => ({
fontSize: theme.fontSizes.mainHeader,
})}
>
${overageCost.toFixed(2)}
</Typography>
</GridCol>
</GridRow>
}
/>
</Grid>
<StyledDivider />
<Grid container>

View File

@ -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 = ({
</GridColLink>
</Typography>
<StyledInfoLabel>
$5 dollar per 1 million started above
included data
${BILLING_TRAFFIC_BUNDLE_PRICE} per 1
million started above included data
</StyledInfoLabel>
</GridCol>
<GridCol>

View File

@ -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),

View File

@ -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,
),
);
}

View File

@ -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);
});
});

View File

@ -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<string, EndpointInfo> = {
},
};
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 = () => {