B2B-88: add starter kit structure and elements
This commit is contained in:
14
packages/billing/core/README.md
Normal file
14
packages/billing/core/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Billing - @kit/billing
|
||||
|
||||
This package is responsible for managing billing and subscription related operations.
|
||||
|
||||
Make sure the app installs the `@kit/billing` package before using it.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-app",
|
||||
"dependencies": {
|
||||
"@kit/billing": "*"
|
||||
}
|
||||
}
|
||||
```
|
||||
3
packages/billing/core/eslint.config.mjs
Normal file
3
packages/billing/core/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import eslintConfigBase from '@kit/eslint-config/base.js';
|
||||
|
||||
export default eslintConfigBase;
|
||||
1
packages/billing/core/node_modules/@kit/eslint-config
generated
vendored
Symbolic link
1
packages/billing/core/node_modules/@kit/eslint-config
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../tooling/eslint
|
||||
1
packages/billing/core/node_modules/@kit/prettier-config
generated
vendored
Symbolic link
1
packages/billing/core/node_modules/@kit/prettier-config
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../tooling/prettier
|
||||
1
packages/billing/core/node_modules/@kit/supabase
generated
vendored
Symbolic link
1
packages/billing/core/node_modules/@kit/supabase
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../supabase
|
||||
1
packages/billing/core/node_modules/@kit/tsconfig
generated
vendored
Symbolic link
1
packages/billing/core/node_modules/@kit/tsconfig
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../tooling/typescript
|
||||
1
packages/billing/core/node_modules/@kit/ui
generated
vendored
Symbolic link
1
packages/billing/core/node_modules/@kit/ui
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../ui
|
||||
1
packages/billing/core/node_modules/zod
generated
vendored
Symbolic link
1
packages/billing/core/node_modules/zod
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/zod@3.25.56/node_modules/zod
|
||||
33
packages/billing/core/package.json
Normal file
33
packages/billing/core/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@kit/billing",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./components/*": "./src/components/*",
|
||||
"./schema": "./src/schema/index.ts",
|
||||
"./types": "./src/types/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
446
packages/billing/core/src/create-billing-schema.ts
Normal file
446
packages/billing/core/src/create-billing-schema.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export enum LineItemType {
|
||||
Flat = 'flat',
|
||||
PerSeat = 'per_seat',
|
||||
Metered = 'metered',
|
||||
}
|
||||
|
||||
const BillingIntervalSchema = z.enum(['month', 'year']);
|
||||
const LineItemTypeSchema = z.enum(['flat', 'per_seat', 'metered']);
|
||||
|
||||
export const BillingProviderSchema = z.enum([
|
||||
'stripe',
|
||||
'paddle',
|
||||
'lemon-squeezy',
|
||||
]);
|
||||
|
||||
export const PaymentTypeSchema = z.enum(['one-time', 'recurring']);
|
||||
|
||||
export const LineItemSchema = z
|
||||
.object({
|
||||
id: z
|
||||
.string({
|
||||
description:
|
||||
'Unique identifier for the line item. Defined by the Provider.',
|
||||
})
|
||||
.min(1),
|
||||
name: z
|
||||
.string({
|
||||
description: 'Name of the line item. Displayed to the user.',
|
||||
})
|
||||
.min(1),
|
||||
description: z
|
||||
.string({
|
||||
description:
|
||||
'Description of the line item. Displayed to the user and will replace the auto-generated description inferred' +
|
||||
' from the line item. This is useful if you want to provide a more detailed description to the user.',
|
||||
})
|
||||
.optional(),
|
||||
cost: z
|
||||
.number({
|
||||
description: 'Cost of the line item. Displayed to the user.',
|
||||
})
|
||||
.min(0),
|
||||
type: LineItemTypeSchema,
|
||||
unit: z
|
||||
.string({
|
||||
description:
|
||||
'Unit of the line item. Displayed to the user. Example "seat" or "GB"',
|
||||
})
|
||||
.optional(),
|
||||
setupFee: z
|
||||
.number({
|
||||
description: `Lemon Squeezy only: If true, in addition to the cost, a setup fee will be charged.`,
|
||||
})
|
||||
.positive()
|
||||
.optional(),
|
||||
tiers: z
|
||||
.array(
|
||||
z.object({
|
||||
cost: z.number().min(0),
|
||||
upTo: z.union([z.number().min(0), z.literal('unlimited')]),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
data.type !== LineItemType.Metered ||
|
||||
(data.unit && data.tiers !== undefined),
|
||||
{
|
||||
message: 'Metered line items must have a unit and tiers',
|
||||
path: ['type', 'unit', 'tiers'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.type === LineItemType.Metered) {
|
||||
return data.cost === 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Metered line items must have a cost of 0. Please add a different line item type for a flat fee (Stripe)',
|
||||
path: ['type', 'cost'],
|
||||
},
|
||||
);
|
||||
|
||||
export const PlanSchema = z
|
||||
.object({
|
||||
id: z
|
||||
.string({
|
||||
description: 'Unique identifier for the plan. Defined by yourself.',
|
||||
})
|
||||
.min(1),
|
||||
name: z
|
||||
.string({
|
||||
description: 'Name of the plan. Displayed to the user.',
|
||||
})
|
||||
.min(1),
|
||||
interval: BillingIntervalSchema.optional(),
|
||||
custom: z.boolean().default(false).optional(),
|
||||
label: z.string().min(1).optional(),
|
||||
buttonLabel: z.string().min(1).optional(),
|
||||
href: z.string().min(1).optional(),
|
||||
lineItems: z.array(LineItemSchema).refine(
|
||||
(schema) => {
|
||||
const types = schema.map((item) => item.type);
|
||||
|
||||
const perSeat = types.filter(
|
||||
(type) => type === LineItemType.PerSeat,
|
||||
).length;
|
||||
|
||||
const flat = types.filter((type) => type === LineItemType.Flat).length;
|
||||
|
||||
return perSeat <= 1 && flat <= 1;
|
||||
},
|
||||
{
|
||||
message: 'Plans can only have one per-seat and one flat line item',
|
||||
path: ['lineItems'],
|
||||
},
|
||||
),
|
||||
trialDays: z
|
||||
.number({
|
||||
description:
|
||||
'Number of days for the trial period. Leave empty for no trial.',
|
||||
})
|
||||
.positive()
|
||||
.optional(),
|
||||
paymentType: PaymentTypeSchema,
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.custom) {
|
||||
return data.lineItems.length === 0;
|
||||
}
|
||||
|
||||
return data.lineItems.length > 0;
|
||||
},
|
||||
{
|
||||
message: 'Non-Custom Plans must have at least one line item',
|
||||
path: ['lineItems'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.custom) {
|
||||
return data.lineItems.length === 0;
|
||||
}
|
||||
|
||||
return data.lineItems.length > 0;
|
||||
},
|
||||
{
|
||||
message: 'Custom Plans must have 0 line items',
|
||||
path: ['lineItems'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => data.paymentType !== 'one-time' || data.interval === undefined,
|
||||
{
|
||||
message: 'One-time plans must not have an interval',
|
||||
path: ['paymentType', 'interval'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => data.paymentType !== 'recurring' || data.interval !== undefined,
|
||||
{
|
||||
message: 'Recurring plans must have an interval',
|
||||
path: ['paymentType', 'interval'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(item) => {
|
||||
// metered line items can be shared across plans
|
||||
const lineItems = item.lineItems.filter(
|
||||
(item) => item.type !== LineItemType.Metered,
|
||||
);
|
||||
|
||||
const ids = lineItems.map((item) => item.id);
|
||||
|
||||
return ids.length === new Set(ids).size;
|
||||
},
|
||||
{
|
||||
message: 'Line item IDs must be unique',
|
||||
path: ['lineItems'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.paymentType === 'one-time') {
|
||||
const nonFlatLineItems = data.lineItems.filter(
|
||||
(item) => item.type !== LineItemType.Flat,
|
||||
);
|
||||
|
||||
return nonFlatLineItems.length === 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'One-time plans must not have non-flat line items',
|
||||
path: ['paymentType', 'lineItems'],
|
||||
},
|
||||
);
|
||||
|
||||
const ProductSchema = z
|
||||
.object({
|
||||
id: z
|
||||
.string({
|
||||
description:
|
||||
'Unique identifier for the product. Defined by th Provider.',
|
||||
})
|
||||
.min(1),
|
||||
name: z
|
||||
.string({
|
||||
description: 'Name of the product. Displayed to the user.',
|
||||
})
|
||||
.min(1),
|
||||
description: z
|
||||
.string({
|
||||
description: 'Description of the product. Displayed to the user.',
|
||||
})
|
||||
.min(1),
|
||||
currency: z
|
||||
.string({
|
||||
description: 'Currency code for the product. Displayed to the user.',
|
||||
})
|
||||
.min(3)
|
||||
.max(3),
|
||||
badge: z
|
||||
.string({
|
||||
description:
|
||||
'Badge for the product. Displayed to the user. Example: "Popular"',
|
||||
})
|
||||
.optional(),
|
||||
features: z
|
||||
.array(
|
||||
z.string({
|
||||
description: 'Features of the product. Displayed to the user.',
|
||||
}),
|
||||
)
|
||||
.nonempty(),
|
||||
enableDiscountField: z
|
||||
.boolean({
|
||||
description: 'Enable discount field for the product in the checkout.',
|
||||
})
|
||||
.optional(),
|
||||
highlighted: z
|
||||
.boolean({
|
||||
description: 'Highlight this product. Displayed to the user.',
|
||||
})
|
||||
.optional(),
|
||||
hidden: z
|
||||
.boolean({
|
||||
description: 'Hide this product from being displayed to users.',
|
||||
})
|
||||
.optional(),
|
||||
plans: z.array(PlanSchema),
|
||||
})
|
||||
.refine((data) => data.plans.length > 0, {
|
||||
message: 'Products must have at least one plan',
|
||||
path: ['plans'],
|
||||
})
|
||||
.refine(
|
||||
(item) => {
|
||||
const planIds = item.plans.map((plan) => plan.id);
|
||||
|
||||
return planIds.length === new Set(planIds).size;
|
||||
},
|
||||
{
|
||||
message: 'Plan IDs must be unique',
|
||||
path: ['plans'],
|
||||
},
|
||||
);
|
||||
|
||||
const BillingSchema = z
|
||||
.object({
|
||||
provider: BillingProviderSchema,
|
||||
products: z.array(ProductSchema).nonempty(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const ids = data.products.flatMap((product) =>
|
||||
product.plans.flatMap((plan) => plan.lineItems.map((item) => item.id)),
|
||||
);
|
||||
|
||||
return ids.length === new Set(ids).size;
|
||||
},
|
||||
{
|
||||
message: 'Line item IDs must be unique',
|
||||
path: ['products'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(schema) => {
|
||||
if (schema.provider === 'lemon-squeezy') {
|
||||
for (const product of schema.products) {
|
||||
for (const plan of product.plans) {
|
||||
if (plan.lineItems.length > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'Lemon Squeezy only supports one line item per plan',
|
||||
path: ['provider', 'products'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(schema) => {
|
||||
if (schema.provider !== 'lemon-squeezy') {
|
||||
// Check if there are any flat fee metered items
|
||||
const setupFeeItems = schema.products.flatMap((product) =>
|
||||
product.plans.flatMap((plan) =>
|
||||
plan.lineItems.filter((item) => item.setupFee),
|
||||
),
|
||||
);
|
||||
|
||||
// If there are any flat fee metered items, return an error
|
||||
if (setupFeeItems.length > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Setup fee metered items are only supported by Lemon Squeezy. For Stripe and Paddle, please use a separate line item for the setup fee.',
|
||||
path: ['products', 'plans', 'lineItems'],
|
||||
},
|
||||
);
|
||||
|
||||
export function createBillingSchema(config: z.infer<typeof BillingSchema>) {
|
||||
return BillingSchema.parse(config);
|
||||
}
|
||||
|
||||
export type BillingConfig = z.infer<typeof BillingSchema>;
|
||||
export type ProductSchema = z.infer<typeof ProductSchema>;
|
||||
|
||||
export function getPlanIntervals(config: z.infer<typeof BillingSchema>) {
|
||||
const intervals = config.products
|
||||
.flatMap((product) => product.plans.map((plan) => plan.interval))
|
||||
.filter(Boolean);
|
||||
|
||||
return Array.from(new Set(intervals));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,
|
||||
) {
|
||||
for (const product of config.products) {
|
||||
for (const plan of product.plans) {
|
||||
if (plan.id === planId) {
|
||||
// Lemon Squeezy only supports one line item per plan
|
||||
if (config.provider === 'lemon-squeezy') {
|
||||
return plan.lineItems[0];
|
||||
}
|
||||
|
||||
const flatLineItem = plan.lineItems.find(
|
||||
(item) => item.type === LineItemType.Flat,
|
||||
);
|
||||
|
||||
if (flatLineItem) {
|
||||
return flatLineItem;
|
||||
}
|
||||
|
||||
return plan.lineItems[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Base line item not found');
|
||||
}
|
||||
|
||||
export function getProductPlanPair(
|
||||
config: z.infer<typeof BillingSchema>,
|
||||
planId: string,
|
||||
) {
|
||||
for (const product of config.products) {
|
||||
for (const plan of product.plans) {
|
||||
if (plan.id === planId) {
|
||||
return { product, plan };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Plan not found');
|
||||
}
|
||||
|
||||
export function getProductPlanPairByVariantId(
|
||||
config: z.infer<typeof BillingSchema>,
|
||||
planId: string,
|
||||
) {
|
||||
for (const product of config.products) {
|
||||
for (const plan of product.plans) {
|
||||
for (const lineItem of plan.lineItems) {
|
||||
if (lineItem.id === planId) {
|
||||
return { product, plan };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Plan not found');
|
||||
}
|
||||
|
||||
export type PlanTypeMap = Map<string, z.infer<typeof LineItemTypeSchema>>;
|
||||
|
||||
/**
|
||||
* @name getPlanTypesMap
|
||||
* @description Get all line item types for all plans in the config
|
||||
* @param config
|
||||
*/
|
||||
export function getPlanTypesMap(
|
||||
config: z.infer<typeof BillingSchema>,
|
||||
): PlanTypeMap {
|
||||
const planTypes: PlanTypeMap = new Map();
|
||||
|
||||
for (const product of config.products) {
|
||||
for (const plan of product.plans) {
|
||||
for (const lineItem of plan.lineItems) {
|
||||
planTypes.set(lineItem.id, lineItem.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return planTypes;
|
||||
}
|
||||
3
packages/billing/core/src/index.ts
Normal file
3
packages/billing/core/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './create-billing-schema';
|
||||
export * from './services/billing-strategy-provider.service';
|
||||
export * from './services/billing-webhook-handler.service';
|
||||
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CancelSubscriptionParamsSchema = z.object({
|
||||
subscriptionId: z.string(),
|
||||
invoiceNow: z.boolean().optional(),
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CreateBillingPortalSessionSchema = z.object({
|
||||
returnUrl: z.string().url(),
|
||||
customerId: z.string().min(1),
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PlanSchema } from '../create-billing-schema';
|
||||
|
||||
export const CreateBillingCheckoutSchema = z.object({
|
||||
returnUrl: z.string().url(),
|
||||
accountId: z.string().uuid(),
|
||||
plan: PlanSchema,
|
||||
customerId: z.string().optional(),
|
||||
customerEmail: z.string().email().optional(),
|
||||
enableDiscountField: z.boolean().optional(),
|
||||
variantQuantities: z.array(
|
||||
z.object({
|
||||
variantId: z.string().min(1),
|
||||
quantity: z.number(),
|
||||
}),
|
||||
),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
});
|
||||
7
packages/billing/core/src/schema/index.ts
Normal file
7
packages/billing/core/src/schema/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './create-billing-checkout.schema';
|
||||
export * from './create-biling-portal-session.schema';
|
||||
export * from './retrieve-checkout-session.schema';
|
||||
export * from './cancel-subscription-params.schema';
|
||||
export * from './report-billing-usage.schema';
|
||||
export * from './update-subscription-params.schema';
|
||||
export * from './query-billing-usage.schema';
|
||||
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const TimeFilter = z.object(
|
||||
{
|
||||
startTime: z.number(),
|
||||
endTime: z.number(),
|
||||
},
|
||||
{
|
||||
description: `The time range to filter the usage records. Used for Stripe`,
|
||||
},
|
||||
);
|
||||
|
||||
const PageFilter = z.object(
|
||||
{
|
||||
page: z.number(),
|
||||
size: z.number(),
|
||||
},
|
||||
{
|
||||
description: `The page and size to filter the usage records. Used for LS`,
|
||||
},
|
||||
);
|
||||
|
||||
export const QueryBillingUsageSchema = z.object({
|
||||
id: z.string({
|
||||
description:
|
||||
'The id of the usage record. For Stripe a meter ID, for LS a subscription item ID.',
|
||||
}),
|
||||
customerId: z.string({
|
||||
description: 'The id of the customer in the billing system',
|
||||
}),
|
||||
filter: z.union([TimeFilter, PageFilter]),
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ReportBillingUsageSchema = z.object({
|
||||
id: z.string({
|
||||
description:
|
||||
'The id of the usage record. For Stripe a customer ID, for LS a subscription item ID.',
|
||||
}),
|
||||
eventName: z
|
||||
.string({
|
||||
description: 'The name of the event that triggered the usage',
|
||||
})
|
||||
.optional(),
|
||||
usage: z.object({
|
||||
quantity: z.number(),
|
||||
action: z.enum(['increment', 'set']).optional(),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const RetrieveCheckoutSessionSchema = z.object({
|
||||
sessionId: z.string(),
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UpdateSubscriptionParamsSchema = z.object({
|
||||
subscriptionId: z.string().min(1),
|
||||
subscriptionItemId: z.string().min(1),
|
||||
quantity: z.number().min(1),
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
CancelSubscriptionParamsSchema,
|
||||
CreateBillingCheckoutSchema,
|
||||
CreateBillingPortalSessionSchema,
|
||||
QueryBillingUsageSchema,
|
||||
ReportBillingUsageSchema,
|
||||
RetrieveCheckoutSessionSchema,
|
||||
UpdateSubscriptionParamsSchema,
|
||||
} from '../schema';
|
||||
import { UpsertSubscriptionParams } from '../types';
|
||||
|
||||
export abstract class BillingStrategyProviderService {
|
||||
abstract createBillingPortalSession(
|
||||
params: z.infer<typeof CreateBillingPortalSessionSchema>,
|
||||
): Promise<{
|
||||
url: string;
|
||||
}>;
|
||||
|
||||
abstract retrieveCheckoutSession(
|
||||
params: z.infer<typeof RetrieveCheckoutSessionSchema>,
|
||||
): Promise<{
|
||||
checkoutToken: string | null;
|
||||
status: 'complete' | 'expired' | 'open';
|
||||
isSessionOpen: boolean;
|
||||
|
||||
customer: {
|
||||
email: string | null;
|
||||
};
|
||||
}>;
|
||||
|
||||
abstract createCheckoutSession(
|
||||
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
||||
): Promise<{
|
||||
checkoutToken: string;
|
||||
}>;
|
||||
|
||||
abstract cancelSubscription(
|
||||
params: z.infer<typeof CancelSubscriptionParamsSchema>,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
|
||||
abstract reportUsage(
|
||||
params: z.infer<typeof ReportBillingUsageSchema>,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
|
||||
abstract queryUsage(
|
||||
params: z.infer<typeof QueryBillingUsageSchema>,
|
||||
): Promise<{
|
||||
value: number;
|
||||
}>;
|
||||
|
||||
abstract updateSubscriptionItem(
|
||||
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
|
||||
abstract getPlanById(planId: string): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
interval: string;
|
||||
amount: number;
|
||||
}>;
|
||||
|
||||
abstract getSubscription(subscriptionId: string): Promise<
|
||||
UpsertSubscriptionParams & {
|
||||
// we can't always guarantee that the target account id will be present
|
||||
// so we need to make it optional and let the consumer handle it
|
||||
target_account_id: string | undefined;
|
||||
}
|
||||
>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { UpsertOrderParams, UpsertSubscriptionParams } from '../types';
|
||||
|
||||
/**
|
||||
* @name BillingWebhookHandlerService
|
||||
* @description Represents an abstract class for handling billing webhook events.
|
||||
*/
|
||||
export abstract class BillingWebhookHandlerService {
|
||||
/**
|
||||
* @name verifyWebhookSignature
|
||||
* @description Verify the webhook signature
|
||||
* @param request
|
||||
*/
|
||||
abstract verifyWebhookSignature(request: Request): Promise<unknown>;
|
||||
|
||||
/**
|
||||
* @name handleWebhookEvent
|
||||
* @description Handle the webhook event from the billing provider
|
||||
* @param event
|
||||
* @param params
|
||||
*/
|
||||
abstract handleWebhookEvent(
|
||||
event: unknown,
|
||||
params: {
|
||||
// this method is called when a checkout session is completed
|
||||
onCheckoutSessionCompleted: (
|
||||
subscription: UpsertSubscriptionParams | UpsertOrderParams,
|
||||
) => Promise<unknown>;
|
||||
|
||||
// this method is called when a subscription is updated
|
||||
onSubscriptionUpdated: (
|
||||
subscription: UpsertSubscriptionParams,
|
||||
) => Promise<unknown>;
|
||||
|
||||
// this method is called when a subscription is deleted
|
||||
onSubscriptionDeleted: (subscriptionId: string) => Promise<unknown>;
|
||||
|
||||
// this method is called when a payment is succeeded. This is used for
|
||||
// one-time payments
|
||||
onPaymentSucceeded: (sessionId: string) => Promise<unknown>;
|
||||
|
||||
// this method is called when a payment is failed. This is used for
|
||||
// one-time payments
|
||||
onPaymentFailed: (sessionId: string) => Promise<unknown>;
|
||||
|
||||
// this method is called when an invoice is paid. We don't have a specific use case for this
|
||||
// but it's extremely common for credit-based systems
|
||||
onInvoicePaid: (
|
||||
subscription: UpsertSubscriptionParams,
|
||||
) => Promise<unknown>;
|
||||
|
||||
// generic handler for any event
|
||||
onEvent?: (data: unknown) => Promise<unknown>;
|
||||
},
|
||||
): Promise<unknown>;
|
||||
}
|
||||
22
packages/billing/core/src/types/index.ts
Normal file
22
packages/billing/core/src/types/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export type UpsertSubscriptionParams =
|
||||
Database['public']['Functions']['upsert_subscription']['Args'] & {
|
||||
line_items: Array<LineItem>;
|
||||
};
|
||||
|
||||
interface LineItem {
|
||||
id: string;
|
||||
quantity: number;
|
||||
subscription_id: string;
|
||||
subscription_item_id: string;
|
||||
product_id: string;
|
||||
variant_id: string;
|
||||
price_amount: number | null | undefined;
|
||||
interval: string;
|
||||
interval_count: number;
|
||||
type: 'flat' | 'metered' | 'per_seat' | undefined;
|
||||
}
|
||||
|
||||
export type UpsertOrderParams =
|
||||
Database['public']['Functions']['upsert_order']['Args'];
|
||||
8
packages/billing/core/tsconfig.json
Normal file
8
packages/billing/core/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user