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:
parent
332440491a
commit
b7af9b7ec3
@ -6,9 +6,15 @@ import { GridColLink } from './GridColLink/GridColLink';
|
|||||||
import type { IInstanceStatus } from 'interfaces/instance';
|
import type { IInstanceStatus } from 'interfaces/instance';
|
||||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
import {
|
import {
|
||||||
|
BILLING_INCLUDED_REQUESTS,
|
||||||
BILLING_PAYG_DEFAULT_MINIMUM_SEATS,
|
BILLING_PAYG_DEFAULT_MINIMUM_SEATS,
|
||||||
BILLING_PAYG_USER_PRICE,
|
BILLING_PAYG_USER_PRICE,
|
||||||
|
BILLING_TRAFFIC_BUNDLE_PRICE,
|
||||||
} from './BillingPlan';
|
} 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 }) => ({
|
const StyledInfoLabel = styled(Typography)(({ theme }) => ({
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
@ -27,6 +33,14 @@ export const BillingDetailsPAYG = ({
|
|||||||
instanceStatus,
|
instanceStatus,
|
||||||
}: IBillingDetailsPAYGProps) => {
|
}: IBillingDetailsPAYGProps) => {
|
||||||
const { users, loading } = useUsers();
|
const { users, loading } = useUsers();
|
||||||
|
const {
|
||||||
|
currentPeriod,
|
||||||
|
toChartData,
|
||||||
|
toTrafficUsageSum,
|
||||||
|
endpointsInfo,
|
||||||
|
getDayLabels,
|
||||||
|
calculateOverageCost,
|
||||||
|
} = useTrafficDataEstimation();
|
||||||
|
|
||||||
const eligibleUsers = users.filter((user) => user.email);
|
const eligibleUsers = users.filter((user) => user.email);
|
||||||
|
|
||||||
@ -36,7 +50,27 @@ export const BillingDetailsPAYG = ({
|
|||||||
const billableUsers = Math.max(eligibleUsers.length, minSeats);
|
const billableUsers = Math.max(eligibleUsers.length, minSeats);
|
||||||
const usersCost = BILLING_PAYG_USER_PRICE * billableUsers;
|
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;
|
if (loading) return null;
|
||||||
|
|
||||||
@ -72,6 +106,36 @@ export const BillingDetailsPAYG = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</GridRow>
|
</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>
|
</Grid>
|
||||||
<StyledDivider />
|
<StyledDivider />
|
||||||
<Grid container>
|
<Grid container>
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
BILLING_PLAN_PRICES,
|
BILLING_PLAN_PRICES,
|
||||||
BILLING_PRO_DEFAULT_INCLUDED_SEATS,
|
BILLING_PRO_DEFAULT_INCLUDED_SEATS,
|
||||||
BILLING_PRO_USER_PRICE,
|
BILLING_PRO_USER_PRICE,
|
||||||
|
BILLING_TRAFFIC_BUNDLE_PRICE,
|
||||||
} from './BillingPlan';
|
} from './BillingPlan';
|
||||||
import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
||||||
|
|
||||||
@ -70,7 +71,11 @@ export const BillingDetailsPro = ({
|
|||||||
endpointsInfo,
|
endpointsInfo,
|
||||||
);
|
);
|
||||||
const totalTraffic = toTrafficUsageSum(trafficData);
|
const totalTraffic = toTrafficUsageSum(trafficData);
|
||||||
return calculateOverageCost(totalTraffic, includedTraffic);
|
return calculateOverageCost(
|
||||||
|
totalTraffic,
|
||||||
|
includedTraffic,
|
||||||
|
BILLING_TRAFFIC_BUNDLE_PRICE,
|
||||||
|
);
|
||||||
}, [includedTraffic, traffic, currentPeriod, endpointsInfo]);
|
}, [includedTraffic, traffic, currentPeriod, endpointsInfo]);
|
||||||
|
|
||||||
const totalCost = planPrice + paidAssignedPrice + overageCost;
|
const totalCost = planPrice + paidAssignedPrice + overageCost;
|
||||||
@ -146,8 +151,8 @@ export const BillingDetailsPro = ({
|
|||||||
</GridColLink>
|
</GridColLink>
|
||||||
</Typography>
|
</Typography>
|
||||||
<StyledInfoLabel>
|
<StyledInfoLabel>
|
||||||
$5 dollar per 1 million started above
|
${BILLING_TRAFFIC_BUNDLE_PRICE} per 1
|
||||||
included data
|
million started above included data
|
||||||
</StyledInfoLabel>
|
</StyledInfoLabel>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol>
|
<GridCol>
|
||||||
|
@ -18,6 +18,7 @@ export const BILLING_PAYG_DEFAULT_MINIMUM_SEATS = 5;
|
|||||||
export const BILLING_PRO_USER_PRICE = 15;
|
export const BILLING_PRO_USER_PRICE = 15;
|
||||||
export const BILLING_PRO_DEFAULT_INCLUDED_SEATS = 5;
|
export const BILLING_PRO_DEFAULT_INCLUDED_SEATS = 5;
|
||||||
export const BILLING_INCLUDED_REQUESTS = 53_000_000;
|
export const BILLING_INCLUDED_REQUESTS = 53_000_000;
|
||||||
|
export const BILLING_TRAFFIC_BUNDLE_PRICE = 5;
|
||||||
|
|
||||||
const StyledPlanBox = styled('aside')(({ theme }) => ({
|
const StyledPlanBox = styled('aside')(({ theme }) => ({
|
||||||
padding: theme.spacing(2.5),
|
padding: theme.spacing(2.5),
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin';
|
import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin';
|
||||||
import { formatTickValue } from 'component/common/Chart/formatTickValue';
|
import { formatTickValue } from 'component/common/Chart/formatTickValue';
|
||||||
import { useTrafficLimit } from './hooks/useTrafficLimit';
|
import { useTrafficLimit } from './hooks/useTrafficLimit';
|
||||||
|
import { BILLING_TRAFFIC_BUNDLE_PRICE } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||||
|
|
||||||
const StyledBox = styled(Box)(({ theme }) => ({
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@ -214,6 +215,7 @@ export const NetworkTrafficUsage: VFC = () => {
|
|||||||
const calculatedOverageCost = calculateOverageCost(
|
const calculatedOverageCost = calculateOverageCost(
|
||||||
usage,
|
usage,
|
||||||
includedTraffic,
|
includedTraffic,
|
||||||
|
BILLING_TRAFFIC_BUNDLE_PRICE,
|
||||||
);
|
);
|
||||||
setOverageCost(calculatedOverageCost);
|
setOverageCost(calculatedOverageCost);
|
||||||
|
|
||||||
@ -223,6 +225,7 @@ export const NetworkTrafficUsage: VFC = () => {
|
|||||||
data.datasets,
|
data.datasets,
|
||||||
includedTraffic,
|
includedTraffic,
|
||||||
new Date(),
|
new Date(),
|
||||||
|
BILLING_TRAFFIC_BUNDLE_PRICE,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -101,4 +101,39 @@ describe('traffic overage calculation', () => {
|
|||||||
// 22_500_000 * 3 * 30 = 2_025_000_000
|
// 22_500_000 * 3 * 30 = 2_025_000_000
|
||||||
expect(result).toBe(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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,8 +2,8 @@ import { useState } from 'react';
|
|||||||
import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
||||||
import type { ChartDataset } from 'chart.js';
|
import type { ChartDataset } from 'chart.js';
|
||||||
|
|
||||||
const TRAFFIC_DATA_UNIT_COST = 5;
|
const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5;
|
||||||
const TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
|
const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
|
||||||
|
|
||||||
export type SelectablePeriod = {
|
export type SelectablePeriod = {
|
||||||
key: string;
|
key: string;
|
||||||
@ -39,9 +39,13 @@ const endpointsInfo: Record<string, EndpointInfo> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTrafficDataCost = (trafficData: number) => {
|
const calculateTrafficDataCost = (
|
||||||
const unitCount = Math.ceil(trafficData / TRAFFIC_DATA_UNIT_SIZE);
|
trafficData: number,
|
||||||
return unitCount * TRAFFIC_DATA_UNIT_COST;
|
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 =>
|
const padMonth = (month: number): string =>
|
||||||
@ -167,6 +171,8 @@ const getDayLabels = (dayCount: number): number[] => {
|
|||||||
export const calculateOverageCost = (
|
export const calculateOverageCost = (
|
||||||
dataUsage: number,
|
dataUsage: number,
|
||||||
includedTraffic: number,
|
includedTraffic: number,
|
||||||
|
trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST,
|
||||||
|
trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE,
|
||||||
): number => {
|
): number => {
|
||||||
if (dataUsage === 0) {
|
if (dataUsage === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -174,7 +180,9 @@ export const calculateOverageCost = (
|
|||||||
|
|
||||||
const overage =
|
const overage =
|
||||||
Math.floor((dataUsage - includedTraffic) / 1_000_000) * 1_000_000;
|
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 = (
|
export const calculateProjectedUsage = (
|
||||||
@ -203,6 +211,8 @@ export const calculateEstimatedMonthlyCost = (
|
|||||||
trafficData: ChartDatasetType[],
|
trafficData: ChartDatasetType[],
|
||||||
includedTraffic: number,
|
includedTraffic: number,
|
||||||
currentDate: Date,
|
currentDate: Date,
|
||||||
|
trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST,
|
||||||
|
trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE,
|
||||||
) => {
|
) => {
|
||||||
if (period !== currentPeriod.key) {
|
if (period !== currentPeriod.key) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -214,7 +224,12 @@ export const calculateEstimatedMonthlyCost = (
|
|||||||
trafficData,
|
trafficData,
|
||||||
currentPeriod.dayCount,
|
currentPeriod.dayCount,
|
||||||
);
|
);
|
||||||
return calculateOverageCost(projectedUsage, includedTraffic);
|
return calculateOverageCost(
|
||||||
|
projectedUsage,
|
||||||
|
includedTraffic,
|
||||||
|
trafficUnitCost,
|
||||||
|
trafficUnitSize,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTrafficDataEstimation = () => {
|
export const useTrafficDataEstimation = () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user