pull/720/head
Travis Fischer 2025-07-05 13:26:43 -07:00
rodzic c88bc60945
commit 02d841aa54
4 zmienionych plików z 122 dodań i 64 usunięć

Wyświetl plik

@ -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:*",

Wyświetl plik

@ -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

Wyświetl plik

@ -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')
})

Wyświetl plik

@ -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
}