mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Merge pull request #6579 from supabase/feat/quota-billing
Feat/quota billing
This commit is contained in:
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>
|
||||
</>
|
||||
|
||||
Reference in new issue
Block a user