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 { 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> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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), | ||||
|  | ||||
| @ -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, | ||||
|                     ), | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
| @ -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); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @ -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 = () => { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user