mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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