kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/720/head
rodzic
c88bc60945
commit
02d841aa54
|
@ -17,7 +17,8 @@
|
|||
"start": "next start",
|
||||
"clean": "del .next",
|
||||
"test": "run-s test:*",
|
||||
"test:lint": "next lint"
|
||||
"test:lint": "next lint",
|
||||
"test:unit": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentic/platform": "workspace:*",
|
||||
|
|
|
@ -6,9 +6,11 @@ import type {
|
|||
} from '@agentic/platform-types'
|
||||
import humanNumber from 'human-number'
|
||||
import { Loader2Icon, PlusIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import plur from 'plur'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { pricingAmountToFixedString } from '@/lib/utils'
|
||||
|
||||
// const intervalToLabelMap: Record<PricingInterval, string> = {
|
||||
// day: 'daily',
|
||||
|
@ -39,15 +41,14 @@ export function ProjectPricingPlan({
|
|||
const requestsLineItem = lineItems.find(
|
||||
(lineItem) => lineItem.slug === 'requests'
|
||||
)
|
||||
// TODO
|
||||
// TODO: support custom line-items
|
||||
// const customLineItems = lineItems.find(
|
||||
// (lineItem) => lineItem.slug !== 'base' && lineItem.slug !== 'requests'
|
||||
// )
|
||||
|
||||
// TODO: support transformQuantity.divideBy
|
||||
// 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 (
|
||||
<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='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 className='text-sm text-gray-600'>/ {interval}</div>
|
||||
|
@ -77,10 +81,7 @@ export function ProjectPricingPlan({
|
|||
{requestsLineItem.billingScheme === 'per_unit' ? (
|
||||
<div className='flex flex-row items-center gap-2'>
|
||||
<div className='text-xl font-semibold text-gray-950 leading-none py-2'>
|
||||
$
|
||||
{requestsLineItem.unitAmount <= 0
|
||||
? 0
|
||||
: (requestsLineItem.unitAmount / 100).toFixed(2)}
|
||||
${pricingAmountToFixedString(requestsLineItem.unitAmount)}
|
||||
</div>
|
||||
|
||||
<div className='text-sm text-gray-600'>
|
||||
|
@ -91,68 +92,58 @@ export function ProjectPricingPlan({
|
|||
</div>
|
||||
</div>
|
||||
) : requestsLineItem.billingScheme === 'tiered' ? (
|
||||
requestsLineItem.tiersMode === 'graduated' ? (
|
||||
<div>TODO</div>
|
||||
) : requestsLineItem.tiersMode === 'volume' ? (
|
||||
<div>
|
||||
{requestsLineItem.tiers?.map((tier, index) => {
|
||||
const isFirst = index === 0
|
||||
// const isLast = index >= requestsLineItem.tiers!.length - 1
|
||||
const hasUnitAmount = tier.unitAmount !== undefined
|
||||
// const hasFlatAmount = tier.flatAmount !== undefined
|
||||
const isFree = hasUnitAmount
|
||||
? // TODO: are these two mutually exclusive? check in stripe
|
||||
tier.unitAmount === 0
|
||||
: tier.flatAmount === 0
|
||||
<div>
|
||||
{requestsLineItem.tiers?.map((tier, index) => {
|
||||
const isFirst = index === 0
|
||||
// const isLast = index >= requestsLineItem.tiers!.length - 1
|
||||
const hasUnitAmount = tier.unitAmount !== undefined
|
||||
// const hasFlatAmount = tier.flatAmount !== undefined
|
||||
const isFree = hasUnitAmount
|
||||
? // TODO: are these two mutually exclusive? check in stripe
|
||||
tier.unitAmount === 0
|
||||
: tier.flatAmount === 0
|
||||
|
||||
// TODO: improve `inf` label
|
||||
const numLabel =
|
||||
tier.upTo === 'inf'
|
||||
? 'infinite requests'
|
||||
: `${humanNumber(tier.upTo)} ${plur('request', tier.upTo)}`
|
||||
// TODO: improve `inf` label
|
||||
const numLabel =
|
||||
tier.upTo === 'inf'
|
||||
? 'infinite requests'
|
||||
: `${humanNumber(tier.upTo)} ${plur('request', tier.upTo)}`
|
||||
|
||||
return (
|
||||
<div key={index} className=''>
|
||||
{isFree ? (
|
||||
isFirst ? (
|
||||
<div>FREE for the first {numLabel}</div>
|
||||
) : (
|
||||
<div>
|
||||
$
|
||||
{hasUnitAmount
|
||||
? (tier.unitAmount! / 100)
|
||||
.toFixed(10)
|
||||
.replace(/0+$/, '')
|
||||
: (tier.flatAmount! / 100)
|
||||
.toFixed(10)
|
||||
.replace(/0+$/, '')}{' '}
|
||||
{hasUnitAmount ? `per request ` : ''}up to{' '}
|
||||
{numLabel}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div key={index} className=''>
|
||||
{isFree ? (
|
||||
isFirst ? (
|
||||
<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+$/, '')}{' '}
|
||||
{pricingAmountToFixedString(
|
||||
hasUnitAmount
|
||||
? tier.unitAmount!
|
||||
: tier.flatAmount!
|
||||
)}{' '}
|
||||
{hasUnitAmount ? `per request ` : ''}up to{' '}
|
||||
{numLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
Unsupported pricing config. Please contact support.
|
||||
</div>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
$
|
||||
{pricingAmountToFixedString(
|
||||
hasUnitAmount
|
||||
? tier.unitAmount!
|
||||
: tier.flatAmount!
|
||||
)}{' '}
|
||||
{hasUnitAmount ? `per request ` : ''}up to{' '}
|
||||
{numLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div>Unsupported pricing config. Please contact support.</div>
|
||||
)}
|
||||
|
@ -169,7 +160,7 @@ export function ProjectPricingPlan({
|
|||
{plan.features.map((feature, index) => (
|
||||
<li
|
||||
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'>
|
||||
<PlusIcon
|
||||
|
@ -184,6 +175,38 @@ export function ProjectPricingPlan({
|
|||
</ul>
|
||||
</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>
|
||||
|
||||
<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) {
|
||||
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