Replace 'base' line item type with 'flat'
This commit updates all instances of 'base' line item type to 'flat'. It modifies the BillingIntervalSchema, the validation rules for one-time plans, and the function to get the primary line item for a plan. Furthermore, it adjusts the display and filtering of line items in the pricing table component and the plan picker component. The SQL migration script and the sample billing configuration are also updated to reflect this change.
This commit is contained in:
@@ -32,7 +32,7 @@ export default createBillingSchema({
|
||||
id: '324643',
|
||||
name: 'Base',
|
||||
cost: 999.99,
|
||||
type: 'base',
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -53,33 +53,23 @@ export default createBillingSchema({
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
{
|
||||
id: '55476',
|
||||
name: 'Base',
|
||||
id: '324646',
|
||||
name: 'Addon 2',
|
||||
cost: 9.99,
|
||||
type: 'base',
|
||||
},
|
||||
{
|
||||
id: '324644',
|
||||
name: 'Addon 1',
|
||||
cost: 99.99,
|
||||
type: 'metered',
|
||||
unit: 'GB',
|
||||
unit: 'GBs',
|
||||
tiers: [
|
||||
{
|
||||
upTo: 5,
|
||||
cost: 0,
|
||||
},
|
||||
{
|
||||
upTo: 10,
|
||||
cost: 0.99,
|
||||
},
|
||||
{
|
||||
upTo: 100,
|
||||
cost: 0.49,
|
||||
},
|
||||
{
|
||||
upTo: 1000,
|
||||
cost: 0.29,
|
||||
cost: 6.99,
|
||||
},
|
||||
{
|
||||
upTo: 'unlimited',
|
||||
cost: 0.19,
|
||||
cost: 0.49,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -115,7 +105,7 @@ export default createBillingSchema({
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe1',
|
||||
name: 'Base',
|
||||
cost: 99.99,
|
||||
type: 'base',
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -140,7 +130,7 @@ export default createBillingSchema({
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe2',
|
||||
name: 'Base',
|
||||
cost: 19.99,
|
||||
type: 'base',
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -154,7 +144,7 @@ export default createBillingSchema({
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe3',
|
||||
name: 'Base',
|
||||
cost: 199.99,
|
||||
type: 'base',
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -183,7 +173,7 @@ export default createBillingSchema({
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe4',
|
||||
name: 'Base',
|
||||
cost: 29.99,
|
||||
type: 'base',
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -197,7 +187,7 @@ export default createBillingSchema({
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe5',
|
||||
name: 'Base',
|
||||
cost: 299.99,
|
||||
type: 'base',
|
||||
type: 'flat',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const BillingIntervalSchema = z.enum(['month', 'year']);
|
||||
const LineItemTypeSchema = z.enum(['base', 'per-seat', 'metered']);
|
||||
const LineItemTypeSchema = z.enum(['flat', 'per-seat', 'metered']);
|
||||
|
||||
export const BillingProviderSchema = z.enum([
|
||||
'stripe',
|
||||
@@ -122,15 +122,17 @@ export const PlanSchema = z
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.paymentType === 'one-time') {
|
||||
const baseItems = data.lineItems.filter((item) => item.type !== 'base');
|
||||
const nonFlatLineItems = data.lineItems.filter(
|
||||
(item) => item.type !== 'flat',
|
||||
);
|
||||
|
||||
return baseItems.length === 0;
|
||||
return nonFlatLineItems.length === 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'One-time plans must not have non-base line items',
|
||||
message: 'One-time plans must not have non-flat line items',
|
||||
path: ['paymentType', 'lineItems'],
|
||||
},
|
||||
);
|
||||
@@ -266,7 +268,17 @@ export function getPlanIntervals(config: z.infer<typeof BillingSchema>) {
|
||||
return Array.from(new Set(intervals));
|
||||
}
|
||||
|
||||
export function getBaseLineItem(
|
||||
/**
|
||||
* @name getPrimaryLineItem
|
||||
* @description Get the primary line item for a plan
|
||||
* By default, the primary line item is the first line item in the plan for Lemon Squeezy
|
||||
* For other providers, the primary line item is the first flat line item in the plan. If there are no flat line items,
|
||||
* the first line item is returned.
|
||||
*
|
||||
* @param config
|
||||
* @param planId
|
||||
*/
|
||||
export function getPrimaryLineItem(
|
||||
config: z.infer<typeof BillingSchema>,
|
||||
planId: string,
|
||||
) {
|
||||
@@ -278,11 +290,15 @@ export function getBaseLineItem(
|
||||
return plan.lineItems[0];
|
||||
}
|
||||
|
||||
const item = plan.lineItems.find((item) => item.type === 'base');
|
||||
const flatLineItem = plan.lineItems.find(
|
||||
(item) => item.type === 'flat',
|
||||
);
|
||||
|
||||
if (item) {
|
||||
return item;
|
||||
if (flatLineItem) {
|
||||
return flatLineItem;
|
||||
}
|
||||
|
||||
return plan.lineItems[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import { z } from 'zod';
|
||||
import {
|
||||
BillingConfig,
|
||||
LineItemSchema,
|
||||
getBaseLineItem,
|
||||
getPlanIntervals,
|
||||
getPrimaryLineItem,
|
||||
getProductPlanPair,
|
||||
} from '@kit/billing';
|
||||
import { formatCurrency } from '@kit/shared/utils';
|
||||
@@ -214,7 +214,7 @@ export function PlanPicker(
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseLineItem = getBaseLineItem(
|
||||
const baseLineItem = getPrimaryLineItem(
|
||||
props.config,
|
||||
plan.id,
|
||||
);
|
||||
|
||||
@@ -10,8 +10,8 @@ import { z } from 'zod';
|
||||
import {
|
||||
BillingConfig,
|
||||
LineItemSchema,
|
||||
getBaseLineItem,
|
||||
getPlanIntervals,
|
||||
getPrimaryLineItem,
|
||||
} from '@kit/billing';
|
||||
import { formatCurrency } from '@kit/shared/utils';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
@@ -79,9 +79,9 @@ export function PricingTable({
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseLineItem = getBaseLineItem(config, plan.id);
|
||||
const primaryLineItem = getPrimaryLineItem(config, plan.id);
|
||||
|
||||
if (!baseLineItem) {
|
||||
if (!primaryLineItem) {
|
||||
throw new Error(`Base line item was not found`);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export function PricingTable({
|
||||
selectable
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
baseLineItem={baseLineItem}
|
||||
primaryLineItem={primaryLineItem}
|
||||
product={product}
|
||||
paths={paths}
|
||||
displayPlanDetails={displayPlanDetails}
|
||||
@@ -118,10 +118,7 @@ function PricingItem(
|
||||
|
||||
selectable: boolean;
|
||||
|
||||
baseLineItem: {
|
||||
id: string;
|
||||
cost: number;
|
||||
};
|
||||
primaryLineItem: z.infer<typeof LineItemSchema>;
|
||||
|
||||
plan: {
|
||||
id: string;
|
||||
@@ -149,10 +146,10 @@ function PricingItem(
|
||||
) {
|
||||
const highlighted = props.product.highlighted ?? false;
|
||||
|
||||
// we want to exclude the base plan from the list of line items
|
||||
// since we are displaying the base plan separately as the main price
|
||||
// we want to exclude the primary plan from the list of line items
|
||||
// since we are displaying the primary line item separately as the main price
|
||||
const lineItemsToDisplay = props.plan.lineItems.filter((item) => {
|
||||
return item.type !== 'base';
|
||||
return item.id !== props.primaryLineItem.id;
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -171,7 +168,12 @@ function PricingItem(
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div className={'flex items-center space-x-4'}>
|
||||
<Heading level={4}>
|
||||
<b className={'font-bold'}>{props.product.name}</b>
|
||||
<b className={'font-bold'}>
|
||||
<Trans
|
||||
i18nKey={props.product.name}
|
||||
defaults={props.product.name}
|
||||
/>
|
||||
</b>
|
||||
</Heading>
|
||||
|
||||
<If condition={props.product.badge}>
|
||||
@@ -209,13 +211,13 @@ function PricingItem(
|
||||
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<Price>
|
||||
{formatCurrency(props.product.currency, props.baseLineItem.cost)}
|
||||
{formatCurrency(props.product.currency, props.primaryLineItem.cost)}
|
||||
</Price>
|
||||
|
||||
<If condition={props.plan.name}>
|
||||
<span
|
||||
className={cn(
|
||||
`animate-in slide-in-from-left-4 fade-in capitalize`,
|
||||
`animate-in slide-in-from-left-4 fade-in flex items-center space-x-0.5 capitalize`,
|
||||
{
|
||||
['text-muted-foreground']: !highlighted,
|
||||
},
|
||||
@@ -231,6 +233,32 @@ function PricingItem(
|
||||
)}
|
||||
</If>
|
||||
</span>
|
||||
|
||||
<If condition={props.primaryLineItem.type !== 'flat'}>
|
||||
<span>/</span>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
`animate-in slide-in-from-left-4 fade-in text-sm capitalize`,
|
||||
{
|
||||
['text-muted-foreground']: !highlighted,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<If condition={props.primaryLineItem.type === 'per-seat'}>
|
||||
<Trans i18nKey={'billing:perTeamMember'} />
|
||||
</If>
|
||||
|
||||
<If condition={props.primaryLineItem.unit}>
|
||||
<Trans
|
||||
i18nKey={'billing:perUnit'}
|
||||
values={{
|
||||
unit: props.primaryLineItem.unit,
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
</span>
|
||||
</If>
|
||||
</span>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
@@ -78,4 +78,6 @@ export const POST = enhanceRouteHandler(({ request, body, user }) => {
|
||||
id: z.number()
|
||||
}),
|
||||
});
|
||||
```
|
||||
```
|
||||
|
||||
When using a Captcha, the consumer will pass an header `x-captcha-token` with the captcha token.
|
||||
@@ -123,7 +123,7 @@ create type public.billing_provider as ENUM(
|
||||
- You can add more types as needed.
|
||||
*/
|
||||
create type public.subscription_item_type as ENUM(
|
||||
'base',
|
||||
'flat',
|
||||
'per_seat',
|
||||
'metered'
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user