kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/720/head
rodzic
c88bc60945
commit
02d841aa54
|
@ -17,7 +17,8 @@
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"clean": "del .next",
|
"clean": "del .next",
|
||||||
"test": "run-s test:*",
|
"test": "run-s test:*",
|
||||||
"test:lint": "next lint"
|
"test:lint": "next lint",
|
||||||
|
"test:unit": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentic/platform": "workspace:*",
|
"@agentic/platform": "workspace:*",
|
||||||
|
|
|
@ -6,9 +6,11 @@ import type {
|
||||||
} from '@agentic/platform-types'
|
} from '@agentic/platform-types'
|
||||||
import humanNumber from 'human-number'
|
import humanNumber from 'human-number'
|
||||||
import { Loader2Icon, PlusIcon } from 'lucide-react'
|
import { Loader2Icon, PlusIcon } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
import plur from 'plur'
|
import plur from 'plur'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { pricingAmountToFixedString } from '@/lib/utils'
|
||||||
|
|
||||||
// const intervalToLabelMap: Record<PricingInterval, string> = {
|
// const intervalToLabelMap: Record<PricingInterval, string> = {
|
||||||
// day: 'daily',
|
// day: 'daily',
|
||||||
|
@ -39,15 +41,14 @@ export function ProjectPricingPlan({
|
||||||
const requestsLineItem = lineItems.find(
|
const requestsLineItem = lineItems.find(
|
||||||
(lineItem) => lineItem.slug === 'requests'
|
(lineItem) => lineItem.slug === 'requests'
|
||||||
)
|
)
|
||||||
// TODO
|
// TODO: support custom line-items
|
||||||
// const customLineItems = lineItems.find(
|
// const customLineItems = lineItems.find(
|
||||||
// (lineItem) => lineItem.slug !== 'base' && lineItem.slug !== 'requests'
|
// (lineItem) => lineItem.slug !== 'base' && lineItem.slug !== 'requests'
|
||||||
// )
|
// )
|
||||||
|
|
||||||
// TODO: support transformQuantity.divideBy
|
|
||||||
// TODO: support defaultAggregation
|
// TODO: support defaultAggregation
|
||||||
|
|
||||||
// TODO: toFixed(2) doesn't work for usage-based pricing with small amounts
|
// TODO: add rate-limits and finesse free tier to not be so basic
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='justify-self-center w-full grid grid-cols-1 rounded-[2rem] shadow-[inset_0_0_2px_1px_#ffffff4d] ring-1 ring-black/5 max-lg:mx-auto max-lg:w-full max-lg:max-w-md max-w-lg'>
|
<div className='justify-self-center w-full grid grid-cols-1 rounded-[2rem] shadow-[inset_0_0_2px_1px_#ffffff4d] ring-1 ring-black/5 max-lg:mx-auto max-lg:w-full max-lg:max-w-md max-w-lg'>
|
||||||
|
@ -62,7 +63,10 @@ export function ProjectPricingPlan({
|
||||||
|
|
||||||
<div className='flex flex-row items-center gap-2'>
|
<div className='flex flex-row items-center gap-2'>
|
||||||
<div className='text-4xl font-semibold text-gray-950 leading-none py-2'>
|
<div className='text-4xl font-semibold text-gray-950 leading-none py-2'>
|
||||||
${baseLineItem ? (baseLineItem.amount / 100).toFixed(2) : 0}
|
$
|
||||||
|
{baseLineItem
|
||||||
|
? pricingAmountToFixedString(baseLineItem.amount)
|
||||||
|
: 0}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='text-sm text-gray-600'>/ {interval}</div>
|
<div className='text-sm text-gray-600'>/ {interval}</div>
|
||||||
|
@ -77,10 +81,7 @@ export function ProjectPricingPlan({
|
||||||
{requestsLineItem.billingScheme === 'per_unit' ? (
|
{requestsLineItem.billingScheme === 'per_unit' ? (
|
||||||
<div className='flex flex-row items-center gap-2'>
|
<div className='flex flex-row items-center gap-2'>
|
||||||
<div className='text-xl font-semibold text-gray-950 leading-none py-2'>
|
<div className='text-xl font-semibold text-gray-950 leading-none py-2'>
|
||||||
$
|
${pricingAmountToFixedString(requestsLineItem.unitAmount)}
|
||||||
{requestsLineItem.unitAmount <= 0
|
|
||||||
? 0
|
|
||||||
: (requestsLineItem.unitAmount / 100).toFixed(2)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='text-sm text-gray-600'>
|
<div className='text-sm text-gray-600'>
|
||||||
|
@ -91,68 +92,58 @@ export function ProjectPricingPlan({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : requestsLineItem.billingScheme === 'tiered' ? (
|
) : requestsLineItem.billingScheme === 'tiered' ? (
|
||||||
requestsLineItem.tiersMode === 'graduated' ? (
|
<div>
|
||||||
<div>TODO</div>
|
{requestsLineItem.tiers?.map((tier, index) => {
|
||||||
) : requestsLineItem.tiersMode === 'volume' ? (
|
const isFirst = index === 0
|
||||||
<div>
|
// const isLast = index >= requestsLineItem.tiers!.length - 1
|
||||||
{requestsLineItem.tiers?.map((tier, index) => {
|
const hasUnitAmount = tier.unitAmount !== undefined
|
||||||
const isFirst = index === 0
|
// const hasFlatAmount = tier.flatAmount !== undefined
|
||||||
// const isLast = index >= requestsLineItem.tiers!.length - 1
|
const isFree = hasUnitAmount
|
||||||
const hasUnitAmount = tier.unitAmount !== undefined
|
? // TODO: are these two mutually exclusive? check in stripe
|
||||||
// const hasFlatAmount = tier.flatAmount !== undefined
|
tier.unitAmount === 0
|
||||||
const isFree = hasUnitAmount
|
: tier.flatAmount === 0
|
||||||
? // TODO: are these two mutually exclusive? check in stripe
|
|
||||||
tier.unitAmount === 0
|
|
||||||
: tier.flatAmount === 0
|
|
||||||
|
|
||||||
// TODO: improve `inf` label
|
// TODO: improve `inf` label
|
||||||
const numLabel =
|
const numLabel =
|
||||||
tier.upTo === 'inf'
|
tier.upTo === 'inf'
|
||||||
? 'infinite requests'
|
? 'infinite requests'
|
||||||
: `${humanNumber(tier.upTo)} ${plur('request', tier.upTo)}`
|
: `${humanNumber(tier.upTo)} ${plur('request', tier.upTo)}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className=''>
|
<div key={index} className=''>
|
||||||
{isFree ? (
|
{isFree ? (
|
||||||
isFirst ? (
|
isFirst ? (
|
||||||
<div>FREE for the first {numLabel}</div>
|
<div>
|
||||||
) : (
|
FREE for the first {numLabel} per {interval}
|
||||||
<div>
|
</div>
|
||||||
$
|
|
||||||
{hasUnitAmount
|
|
||||||
? (tier.unitAmount! / 100)
|
|
||||||
.toFixed(10)
|
|
||||||
.replace(/0+$/, '')
|
|
||||||
: (tier.flatAmount! / 100)
|
|
||||||
.toFixed(10)
|
|
||||||
.replace(/0+$/, '')}{' '}
|
|
||||||
{hasUnitAmount ? `per request ` : ''}up to{' '}
|
|
||||||
{numLabel}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
$
|
$
|
||||||
{hasUnitAmount
|
{pricingAmountToFixedString(
|
||||||
? (tier.unitAmount! / 100)
|
hasUnitAmount
|
||||||
.toFixed(10)
|
? tier.unitAmount!
|
||||||
.replace(/0+$/, '')
|
: tier.flatAmount!
|
||||||
: (tier.flatAmount! / 100)
|
)}{' '}
|
||||||
.toFixed(10)
|
|
||||||
.replace(/0+$/, '')}{' '}
|
|
||||||
{hasUnitAmount ? `per request ` : ''}up to{' '}
|
{hasUnitAmount ? `per request ` : ''}up to{' '}
|
||||||
{numLabel}
|
{numLabel}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
</div>
|
) : (
|
||||||
)
|
<div>
|
||||||
})}
|
$
|
||||||
</div>
|
{pricingAmountToFixedString(
|
||||||
) : (
|
hasUnitAmount
|
||||||
<div>
|
? tier.unitAmount!
|
||||||
Unsupported pricing config. Please contact support.
|
: tier.flatAmount!
|
||||||
</div>
|
)}{' '}
|
||||||
)
|
{hasUnitAmount ? `per request ` : ''}up to{' '}
|
||||||
|
{numLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>Unsupported pricing config. Please contact support.</div>
|
<div>Unsupported pricing config. Please contact support.</div>
|
||||||
)}
|
)}
|
||||||
|
@ -169,7 +160,7 @@ export function ProjectPricingPlan({
|
||||||
{plan.features.map((feature, index) => (
|
{plan.features.map((feature, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className='group flex flex-row items-start gap-3 text-sm/6 text-gray-600 data-[disabled]:text-gray-400'
|
className='group flex flex-row items-start gap-2 text-sm/6 text-gray-600 data-[disabled]:text-gray-400'
|
||||||
>
|
>
|
||||||
<span className='inline-flex h-6 items-center'>
|
<span className='inline-flex h-6 items-center'>
|
||||||
<PlusIcon
|
<PlusIcon
|
||||||
|
@ -184,6 +175,38 @@ export function ProjectPricingPlan({
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{requestsLineItem?.billingScheme === 'tiered' && (
|
||||||
|
<p className='text-pretty text-xs/5 text-gray-400'>
|
||||||
|
{requestsLineItem.tiersMode === 'graduated' ? (
|
||||||
|
<>
|
||||||
|
Requests pricing tiers use{' '}
|
||||||
|
<Link
|
||||||
|
href='https://docs.stripe.com/products-prices/pricing-models#graduated-pricing'
|
||||||
|
className='link'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
graduated pricing
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Requests pricing tiers use{' '}
|
||||||
|
<Link
|
||||||
|
href='https://docs.stripe.com/products-prices/pricing-models#volume-based-pricing'
|
||||||
|
className='link'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
volume-based pricing
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { expect, test } from 'vitest'
|
||||||
|
|
||||||
|
import { pricingAmountToFixedString } from './utils'
|
||||||
|
|
||||||
|
test('pricingAmountToFixedString', () => {
|
||||||
|
expect(pricingAmountToFixedString(0.008)).toBe('0.00008')
|
||||||
|
expect(pricingAmountToFixedString(200)).toBe('2.00')
|
||||||
|
expect(pricingAmountToFixedString(52_399)).toBe('523.99')
|
||||||
|
expect(pricingAmountToFixedString(0.0)).toBe('0')
|
||||||
|
expect(pricingAmountToFixedString(0)).toBe('0')
|
||||||
|
expect(pricingAmountToFixedString(0.000_000_000_000_1)).toBe('0')
|
||||||
|
expect(pricingAmountToFixedString(0.01)).toBe('0.0001')
|
||||||
|
expect(pricingAmountToFixedString(10)).toBe('0.10')
|
||||||
|
expect(pricingAmountToFixedString(390)).toBe('3.90')
|
||||||
|
expect(pricingAmountToFixedString(1)).toBe('0.01')
|
||||||
|
})
|
|
@ -10,3 +10,21 @@ export function cn(...inputs: ClassValue[]) {
|
||||||
export function randomInRange(min: number, max: number) {
|
export function randomInRange(min: number, max: number) {
|
||||||
return Math.random() * (max - min) + min
|
return Math.random() * (max - min) + min
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pricingAmountToFixedString(amount: number): string {
|
||||||
|
const output = (amount / 100)
|
||||||
|
// Cap the precision to 10 because of floating point issues
|
||||||
|
// (could remove this constraint in the future by not dividing by 100
|
||||||
|
// and handling the precision manually if needed)
|
||||||
|
.toFixed(10)
|
||||||
|
.replace(/0+$/, '')
|
||||||
|
.replace(/\.0*$/, '.00')
|
||||||
|
// (not a $ sign, but a substitution $1)
|
||||||
|
.replace(/(\.\d)$/, '$10')
|
||||||
|
|
||||||
|
if (output === '0.00') {
|
||||||
|
return '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue