Merge pull request #6579 from supabase/feat/quota-billing

Feat/quota billing
This commit is contained in:
Jonathan Summers-Muir authored and GitHub committed 2022-04-27 11:26:24 +08:00
commit 3bb2ec2d07
5 files changed
+61 -33

No files matched your search

@@ -11,6 +11,7 @@ export const chargeableProducts: ChargeableProduct[] = [
costPerUnit: 0.125,
unitQuantity: 1000000000, // 1GB
pricingModel: 'max',
freeQuota: 8000000000, // 8GB
},
{
title: 'Database Egress',
@@ -18,6 +19,7 @@ export const chargeableProducts: ChargeableProduct[] = [
costPerUnit: 0.09,
unitQuantity: 1000000000, // 1GB
pricingModel: 'sum',
freeQuota: 50000000000, // 50GB
},
],
},
@@ -31,6 +33,7 @@ export const chargeableProducts: ChargeableProduct[] = [
costPerUnit: 0.021,
unitQuantity: 1000000000, // 1GB
pricingModel: 'max',
freeQuota: 100000000000, // 100GB
},
{
title: 'Storage Egress',
@@ -38,6 +41,7 @@ export const chargeableProducts: ChargeableProduct[] = [
costPerUnit: 0.09,
unitQuantity: 1000000000, // 1GB
pricingModel: 'sum',
freeQuota: 200000000000, // 200GB
},
],
},
@@ -10,6 +10,7 @@ export interface ProductFeature {
costPerUnit: number
unitQuantity: number
pricingModel: PricingModel
freeQuota?: number
}
export type PaygStats = { [key: string]: { sum: number; max: number } }
@@ -12,7 +12,24 @@ import { ChargeableProduct, PaygStats, ProductFeature } from './PAYGUsage.types'
* @returns number
*/
export const deriveFeatureCost = (paygStats: PaygStats | undefined, feature: ProductFeature) => {
const maximumValueOfTheMonth = paygStats?.[feature.attribute]?.[feature.pricingModel] ?? 0
let rawUsage = paygStats?.[feature.attribute]?.[feature.pricingModel]
/**
* Some features have a free quota
*
* Check there is included free quota,
* and remove free quota amount from the raw usage derived from paygStats
*
* if the number is negative, then Math.max should return 0
*
* todo: move this logic to backend @mildtomato
*/
if (rawUsage && feature.freeQuota) {
rawUsage = Math.max(0, rawUsage - feature.freeQuota)
}
const maximumValueOfTheMonth = rawUsage ?? 0
return (maximumValueOfTheMonth / feature.unitQuantity) * feature.costPerUnit
}
@@ -1,27 +1,33 @@
import { FC } from 'react'
import { Typography } from '@supabase/ui'
import { Badge, Typography } from '@supabase/ui'
interface Props {
item: string
amount: number | string
unitPrice: number | string
price: number | string
freeQuota?: number
}
const CostBreakdownRow: FC<Props> = ({ item, amount, unitPrice, price }) => {
const CostBreakdownRow: FC<Props> = ({ item, amount, unitPrice, price, freeQuota }) => {
return (
<div className="px-6 py-3 relative border-t border-panel-border-light dark:border-panel-border-dark flex items-center">
<div className="w-[40%]">
<Typography.Text>{item}</Typography.Text>
<div className="border-panel-border-light dark:border-panel-border-dark relative flex items-center border-t px-6 py-3">
<div className="flex w-[40%] items-center gap-3">
<span>{item}</span>
{freeQuota && (
<Badge color="scale">
<span className="text-scale-900 text-xs">{`${freeQuota / 1000000000}GB included`}</span>
</Badge>
)}
</div>
<div className="w-[20%] flex justify-end">
<Typography.Text>{amount}</Typography.Text>
<div className="flex w-[20%] justify-end">
<span className="text-sm">{amount}</span>
</div>
<div className="w-[20%] flex justify-end">
<Typography.Text>${unitPrice}</Typography.Text>
<div className="flex w-[20%] justify-end">
<span>${unitPrice}</span>
</div>
<div className="w-[20%] flex justify-end">
<Typography.Text>${price}</Typography.Text>
<div className="flex w-[20%] justify-end">
<span>${price}</span>
</div>
</div>
)
@@ -12,7 +12,7 @@ import CostBreakdownRow from './CostBreakdownRow'
import { StripeSubscription } from './Subscription.types'
import { deriveFeatureCost, deriveProductCost } from '../PAYGUsage/PAYGUsage.utils'
import { chargeableProducts } from '../PAYGUsage/PAYGUsage.constants'
import { PaygStats } from '../PAYGUsage/PAYGUsage.types'
import { PaygStats, ProductFeature } from '../PAYGUsage/PAYGUsage.types'
interface Props {
project: any
@@ -56,14 +56,14 @@ const Subscription: FC<Props> = ({
return (
<Loading active={loading}>
<div className="w-full rounded overflow-hidden border border-panel-border-light dark:border-panel-border-dark mb-8">
<div className="border-panel-border-light dark:border-panel-border-dark mb-8 w-full overflow-hidden rounded border">
<div className="bg-panel-body-light dark:bg-panel-body-dark">
<div className="px-6 pt-4 flex items-center justify-between">
<div className="flex items-center justify-between px-6 pt-4">
<div className="flex flex-col">
<p className="text-scale-1100 text-sm">
{showProjectName ? project.name : 'Current subscription'}
</p>
<h3 className="text-xl mb-0">{subscription?.tier.name ?? '-'}</h3>
<h3 className="mb-0 text-xl">{subscription?.tier.name ?? '-'}</h3>
</div>
<div className="flex flex-col items-end space-y-2">
<Button
@@ -74,7 +74,7 @@ const Subscription: FC<Props> = ({
Change subscription
</Button>
{!isOrgOwner && (
<p className="text-sm text-scale-1100">
<p className="text-scale-1100 text-sm">
Only the organization owner can amend subscriptions
</p>
)}
@@ -89,7 +89,7 @@ const Subscription: FC<Props> = ({
</div>
)}
<div className="mt-2 px-6 pb-4">
<p className="text-sm text-scale-1100">
<p className="text-scale-1100 text-sm">
See our{' '}
<a href="https://supabase.com/pricing" target="_blank" className="underline">
pricing
@@ -101,18 +101,18 @@ const Subscription: FC<Props> = ({
{/* Cost Breakdown */}
{!loading && subscription && (
<>
<div className="px-6 py-3 relative border-t border-panel-border-light dark:border-panel-border-dark flex items-center">
<div className="border-panel-border-light dark:border-panel-border-dark relative flex items-center border-t px-6 py-3">
<div className="w-[40%]">
<p className="text-sm">Item</p>
<p className="text-scale-900 text-xs uppercase">Item</p>
</div>
<div className="w-[20%] flex justify-end">
<p className="text-sm">Amount</p>
<div className="flex w-[20%] justify-end">
<p className="text-scale-900 text-xs uppercase">Amount</p>
</div>
<div className="w-[20%] flex justify-end">
<p className="text-sm">Unit Price</p>
<div className="flex w-[20%] justify-end">
<p className="text-scale-900 text-xs uppercase">Unit Price</p>
</div>
<div className="w-[20%] flex justify-end">
<p className="text-sm">Price</p>
<div className="flex w-[20%] justify-end">
<p className="text-scale-900 text-xs uppercase">Price</p>
</div>
</div>
<CostBreakdownRow
@@ -132,14 +132,14 @@ const Subscription: FC<Props> = ({
))}
{isPayg &&
chargeableProducts.map((product) =>
product.features.map((feature) => {
product.features.map((feature: ProductFeature) => {
const derivedCost = deriveFeatureCost(paygStats, feature)
const cost = derivedCost.toFixed(3)
const cost = derivedCost.toFixed(2)
return (
<CostBreakdownRow
key={feature.attribute}
item={feature.title}
freeQuota={feature.freeQuota}
amount={formatBytes(
paygStats?.[feature.attribute]?.[feature.pricingModel] ?? 0
)}
@@ -149,16 +149,16 @@ const Subscription: FC<Props> = ({
)
})
)}
<div className="px-6 py-3 relative border-t border-panel-border-light dark:border-panel-border-dark flex items-center">
<div className="border-panel-border-light dark:border-panel-border-dark relative flex items-center border-t px-6 py-3">
<div className="w-[80%]">
<p className="text-sm">
<p className="text-scale-1100 text-sm">
Estimated cost for {dayjs.unix(currentPeriodStart).utc().format('MMM D, YYYY')}{' '}
- {dayjs.unix(currentPeriodEnd).utc().format('MMM D, YYYY')} so far
</p>
</div>
<div className="w-[20%] flex justify-end items-center space-x-1">
<div className="flex w-[20%] items-center justify-end space-x-1">
<p className="text-scale-1100">$</p>
<h3 className="text-xl m-0">{deriveTotalCost().toFixed(2)}</h3>
<h3 className="m-0 text-xl">{deriveTotalCost().toFixed(2)}</h3>
</div>
</div>
</>