From 8784a40a69c9eea34933aeb03c2a298dc4b76da9 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Mon, 1 Apr 2024 21:43:18 +0800 Subject: [PATCH] Add Lemon Squeezy Billing System --- package.json | 1 + packages/billing/{ => core}/README.md | 0 packages/billing/{ => core}/package.json | 0 .../{ => core}/src/create-billing-schema.ts | 18 +- packages/billing/{ => core}/src/index.ts | 0 .../cancel-subscription-params.schema.ts | 0 .../create-biling-portal-session.schema.ts | 0 .../schema/create-billing-checkout.schema.ts | 0 .../billing/{ => core}/src/schema/index.ts | 0 .../src/schema/report-billing-usage.schema.ts | 0 .../retrieve-checkout-session.schema.ts | 0 .../billing-strategy-provider.service.ts | 0 .../billing-webhook-handler.service.ts | 0 .../core}/tsconfig.json | 0 .../gateway}/README.md | 0 .../gateway}/package.json | 1 + .../src/components/billing-portal-card.tsx | 0 .../src/components/billing-session-status.tsx | 0 .../current-lifetime-order-card.tsx | 0 .../src/components/current-plan-alert.tsx | 0 .../src/components/current-plan-badge.tsx | 0 .../components/current-subscription-card.tsx | 0 .../src/components/embedded-checkout.tsx | 0 .../gateway}/src/components/index.ts | 0 .../src/components/line-item-details.tsx | 0 .../gateway}/src/components/plan-picker.tsx | 0 .../gateway}/src/components/pricing-table.tsx | 0 .../gateway}/src/index.ts | 0 .../billing-event-handler.service.ts | 0 .../billing-gateway-factory.service.ts | 0 .../billing-gateway-provider-factory.ts | 0 .../billing-gateway-factory.service.ts | 12 +- .../billing-gateway-provider-factory.ts | 0 .../billing-gateway.service.ts | 0 .../billing-webhooks.service.ts | 0 packages/billing/{ => gateway}/tsconfig.json | 0 packages/billing/lemon-squeezy/package.json | 50 ++++++ packages/billing/lemon-squeezy/src/index.ts | 1 + .../schema/lemon-squeezy-server-env.schema.ts | 14 ++ ...te-lemon-squeezy-billing-portal-session.ts | 27 +++ .../services/create-lemon-squeezy-checkout.ts | 82 +++++++++ .../lemon-squeezy-billing-strategy.service.ts | 89 ++++++++++ .../src/services/lemon-squeezy-sdk.ts | 26 +++ .../lemon-squeezy}/tsconfig.json | 0 packages/{ => billing}/stripe/package.json | 0 .../stripe/src/components/index.ts | 0 .../components/stripe-embedded-checkout.tsx | 0 packages/{ => billing}/stripe/src/index.ts | 0 .../src/schema/stripe-client-env.schema.ts | 0 .../src/schema/stripe-server-env.schema.ts | 0 .../create-stripe-billing-portal-session.ts | 0 .../src/services/create-stripe-checkout.ts | 0 .../stripe-billing-strategy.service.ts | 0 .../stripe/src/services/stripe-sdk.ts | 0 .../stripe-webhook-handler.service.ts | 0 packages/billing/stripe/tsconfig.json | 8 + .../stripe/src/types/stripe-webhooks.enum.ts | 9 - pnpm-lock.yaml | 159 +++++++++++------- pnpm-workspace.yaml | 1 + 59 files changed, 424 insertions(+), 74 deletions(-) rename packages/billing/{ => core}/README.md (100%) rename packages/billing/{ => core}/package.json (100%) rename packages/billing/{ => core}/src/create-billing-schema.ts (93%) rename packages/billing/{ => core}/src/index.ts (100%) rename packages/billing/{ => core}/src/schema/cancel-subscription-params.schema.ts (100%) rename packages/billing/{ => core}/src/schema/create-biling-portal-session.schema.ts (100%) rename packages/billing/{ => core}/src/schema/create-billing-checkout.schema.ts (100%) rename packages/billing/{ => core}/src/schema/index.ts (100%) rename packages/billing/{ => core}/src/schema/report-billing-usage.schema.ts (100%) rename packages/billing/{ => core}/src/schema/retrieve-checkout-session.schema.ts (100%) rename packages/billing/{ => core}/src/services/billing-strategy-provider.service.ts (100%) rename packages/billing/{ => core}/src/services/billing-webhook-handler.service.ts (100%) rename packages/{billing-gateway => billing/core}/tsconfig.json (100%) rename packages/{billing-gateway => billing/gateway}/README.md (100%) rename packages/{billing-gateway => billing/gateway}/package.json (96%) rename packages/{billing-gateway => billing/gateway}/src/components/billing-portal-card.tsx (100%) rename packages/{billing-gateway => billing/gateway}/src/components/billing-session-status.tsx (100%) rename packages/{billing-gateway => billing/gateway}/src/components/current-lifetime-order-card.tsx (100%) rename packages/{billing-gateway => billing/gateway}/src/components/current-plan-alert.tsx (100%) rename packages/{billing-gateway => billing/gateway}/src/components/current-plan-badge.tsx (100%) rename packages/{billing-gateway => billing/gateway}/src/components/current-subscription-card.tsx (100%) rename packages/{billing-gateway => billing/gateway}/src/components/embedded-checkout.tsx (100%) rename packages/{billing-gateway => billing/gateway}/src/components/index.ts (100%) rename packages/{billing-gateway => billing/gateway}/src/components/line-item-details.tsx (100%) rename packages/{billing-gateway => billing/gateway}/src/components/plan-picker.tsx (100%) rename packages/{billing-gateway => billing/gateway}/src/components/pricing-table.tsx (100%) rename packages/{billing-gateway => billing/gateway}/src/index.ts (100%) rename packages/{billing-gateway => billing/gateway}/src/server/services/billing-event-handler/billing-event-handler.service.ts (100%) rename packages/{billing-gateway => billing/gateway}/src/server/services/billing-event-handler/billing-gateway-factory.service.ts (100%) rename packages/{billing-gateway => billing/gateway}/src/server/services/billing-event-handler/billing-gateway-provider-factory.ts (100%) rename packages/{billing-gateway => billing/gateway}/src/server/services/billing-gateway/billing-gateway-factory.service.ts (80%) rename packages/{billing-gateway => billing/gateway}/src/server/services/billing-gateway/billing-gateway-provider-factory.ts (100%) rename packages/{billing-gateway => billing/gateway}/src/server/services/billing-gateway/billing-gateway.service.ts (100%) rename packages/{billing-gateway => billing/gateway}/src/server/services/billing-webhooks/billing-webhooks.service.ts (100%) rename packages/billing/{ => gateway}/tsconfig.json (100%) create mode 100644 packages/billing/lemon-squeezy/package.json create mode 100644 packages/billing/lemon-squeezy/src/index.ts create mode 100644 packages/billing/lemon-squeezy/src/schema/lemon-squeezy-server-env.schema.ts create mode 100644 packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-billing-portal-session.ts create mode 100644 packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-checkout.ts create mode 100644 packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts create mode 100644 packages/billing/lemon-squeezy/src/services/lemon-squeezy-sdk.ts rename packages/{stripe => billing/lemon-squeezy}/tsconfig.json (100%) rename packages/{ => billing}/stripe/package.json (100%) rename packages/{ => billing}/stripe/src/components/index.ts (100%) rename packages/{ => billing}/stripe/src/components/stripe-embedded-checkout.tsx (100%) rename packages/{ => billing}/stripe/src/index.ts (100%) rename packages/{ => billing}/stripe/src/schema/stripe-client-env.schema.ts (100%) rename packages/{ => billing}/stripe/src/schema/stripe-server-env.schema.ts (100%) rename packages/{ => billing}/stripe/src/services/create-stripe-billing-portal-session.ts (100%) rename packages/{ => billing}/stripe/src/services/create-stripe-checkout.ts (100%) rename packages/{ => billing}/stripe/src/services/stripe-billing-strategy.service.ts (100%) rename packages/{ => billing}/stripe/src/services/stripe-sdk.ts (100%) rename packages/{ => billing}/stripe/src/services/stripe-webhook-handler.service.ts (100%) create mode 100644 packages/billing/stripe/tsconfig.json delete mode 100644 packages/stripe/src/types/stripe-webhooks.enum.ts diff --git a/package.json b/package.json index 34b08d9aa..68ead1a45 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "apps/*", "packages/*", "packages/features/*", + "packages/billing/*", "packages/cms/*", "tooling/*", "supabase" diff --git a/packages/billing/README.md b/packages/billing/core/README.md similarity index 100% rename from packages/billing/README.md rename to packages/billing/core/README.md diff --git a/packages/billing/package.json b/packages/billing/core/package.json similarity index 100% rename from packages/billing/package.json rename to packages/billing/core/package.json diff --git a/packages/billing/src/create-billing-schema.ts b/packages/billing/core/src/create-billing-schema.ts similarity index 93% rename from packages/billing/src/create-billing-schema.ts rename to packages/billing/core/src/create-billing-schema.ts index 01ffe7a54..f085dddde 100644 --- a/packages/billing/src/create-billing-schema.ts +++ b/packages/billing/core/src/create-billing-schema.ts @@ -176,7 +176,23 @@ const BillingSchema = z 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 { + message: 'Only one line item is allowed for Lemon Squeezy', + path: ['products', 'plans'], + }; + } + } + } + } + + return true; + }); export function createBillingSchema(config: z.infer) { return BillingSchema.parse(config); diff --git a/packages/billing/src/index.ts b/packages/billing/core/src/index.ts similarity index 100% rename from packages/billing/src/index.ts rename to packages/billing/core/src/index.ts diff --git a/packages/billing/src/schema/cancel-subscription-params.schema.ts b/packages/billing/core/src/schema/cancel-subscription-params.schema.ts similarity index 100% rename from packages/billing/src/schema/cancel-subscription-params.schema.ts rename to packages/billing/core/src/schema/cancel-subscription-params.schema.ts diff --git a/packages/billing/src/schema/create-biling-portal-session.schema.ts b/packages/billing/core/src/schema/create-biling-portal-session.schema.ts similarity index 100% rename from packages/billing/src/schema/create-biling-portal-session.schema.ts rename to packages/billing/core/src/schema/create-biling-portal-session.schema.ts diff --git a/packages/billing/src/schema/create-billing-checkout.schema.ts b/packages/billing/core/src/schema/create-billing-checkout.schema.ts similarity index 100% rename from packages/billing/src/schema/create-billing-checkout.schema.ts rename to packages/billing/core/src/schema/create-billing-checkout.schema.ts diff --git a/packages/billing/src/schema/index.ts b/packages/billing/core/src/schema/index.ts similarity index 100% rename from packages/billing/src/schema/index.ts rename to packages/billing/core/src/schema/index.ts diff --git a/packages/billing/src/schema/report-billing-usage.schema.ts b/packages/billing/core/src/schema/report-billing-usage.schema.ts similarity index 100% rename from packages/billing/src/schema/report-billing-usage.schema.ts rename to packages/billing/core/src/schema/report-billing-usage.schema.ts diff --git a/packages/billing/src/schema/retrieve-checkout-session.schema.ts b/packages/billing/core/src/schema/retrieve-checkout-session.schema.ts similarity index 100% rename from packages/billing/src/schema/retrieve-checkout-session.schema.ts rename to packages/billing/core/src/schema/retrieve-checkout-session.schema.ts diff --git a/packages/billing/src/services/billing-strategy-provider.service.ts b/packages/billing/core/src/services/billing-strategy-provider.service.ts similarity index 100% rename from packages/billing/src/services/billing-strategy-provider.service.ts rename to packages/billing/core/src/services/billing-strategy-provider.service.ts diff --git a/packages/billing/src/services/billing-webhook-handler.service.ts b/packages/billing/core/src/services/billing-webhook-handler.service.ts similarity index 100% rename from packages/billing/src/services/billing-webhook-handler.service.ts rename to packages/billing/core/src/services/billing-webhook-handler.service.ts diff --git a/packages/billing-gateway/tsconfig.json b/packages/billing/core/tsconfig.json similarity index 100% rename from packages/billing-gateway/tsconfig.json rename to packages/billing/core/tsconfig.json diff --git a/packages/billing-gateway/README.md b/packages/billing/gateway/README.md similarity index 100% rename from packages/billing-gateway/README.md rename to packages/billing/gateway/README.md diff --git a/packages/billing-gateway/package.json b/packages/billing/gateway/package.json similarity index 96% rename from packages/billing-gateway/package.json rename to packages/billing/gateway/package.json index 263ac109c..76a9b5dbc 100644 --- a/packages/billing-gateway/package.json +++ b/packages/billing/gateway/package.json @@ -29,6 +29,7 @@ "@kit/prettier-config": "workspace:*", "@kit/shared": "workspace:^", "@kit/stripe": "workspace:^", + "@kit/lemon-squeezy": "workspace:^", "@kit/supabase": "workspace:^", "@kit/tailwind-config": "workspace:*", "@kit/tsconfig": "workspace:*", diff --git a/packages/billing-gateway/src/components/billing-portal-card.tsx b/packages/billing/gateway/src/components/billing-portal-card.tsx similarity index 100% rename from packages/billing-gateway/src/components/billing-portal-card.tsx rename to packages/billing/gateway/src/components/billing-portal-card.tsx diff --git a/packages/billing-gateway/src/components/billing-session-status.tsx b/packages/billing/gateway/src/components/billing-session-status.tsx similarity index 100% rename from packages/billing-gateway/src/components/billing-session-status.tsx rename to packages/billing/gateway/src/components/billing-session-status.tsx diff --git a/packages/billing-gateway/src/components/current-lifetime-order-card.tsx b/packages/billing/gateway/src/components/current-lifetime-order-card.tsx similarity index 100% rename from packages/billing-gateway/src/components/current-lifetime-order-card.tsx rename to packages/billing/gateway/src/components/current-lifetime-order-card.tsx diff --git a/packages/billing-gateway/src/components/current-plan-alert.tsx b/packages/billing/gateway/src/components/current-plan-alert.tsx similarity index 100% rename from packages/billing-gateway/src/components/current-plan-alert.tsx rename to packages/billing/gateway/src/components/current-plan-alert.tsx diff --git a/packages/billing-gateway/src/components/current-plan-badge.tsx b/packages/billing/gateway/src/components/current-plan-badge.tsx similarity index 100% rename from packages/billing-gateway/src/components/current-plan-badge.tsx rename to packages/billing/gateway/src/components/current-plan-badge.tsx diff --git a/packages/billing-gateway/src/components/current-subscription-card.tsx b/packages/billing/gateway/src/components/current-subscription-card.tsx similarity index 100% rename from packages/billing-gateway/src/components/current-subscription-card.tsx rename to packages/billing/gateway/src/components/current-subscription-card.tsx diff --git a/packages/billing-gateway/src/components/embedded-checkout.tsx b/packages/billing/gateway/src/components/embedded-checkout.tsx similarity index 100% rename from packages/billing-gateway/src/components/embedded-checkout.tsx rename to packages/billing/gateway/src/components/embedded-checkout.tsx diff --git a/packages/billing-gateway/src/components/index.ts b/packages/billing/gateway/src/components/index.ts similarity index 100% rename from packages/billing-gateway/src/components/index.ts rename to packages/billing/gateway/src/components/index.ts diff --git a/packages/billing-gateway/src/components/line-item-details.tsx b/packages/billing/gateway/src/components/line-item-details.tsx similarity index 100% rename from packages/billing-gateway/src/components/line-item-details.tsx rename to packages/billing/gateway/src/components/line-item-details.tsx diff --git a/packages/billing-gateway/src/components/plan-picker.tsx b/packages/billing/gateway/src/components/plan-picker.tsx similarity index 100% rename from packages/billing-gateway/src/components/plan-picker.tsx rename to packages/billing/gateway/src/components/plan-picker.tsx diff --git a/packages/billing-gateway/src/components/pricing-table.tsx b/packages/billing/gateway/src/components/pricing-table.tsx similarity index 100% rename from packages/billing-gateway/src/components/pricing-table.tsx rename to packages/billing/gateway/src/components/pricing-table.tsx diff --git a/packages/billing-gateway/src/index.ts b/packages/billing/gateway/src/index.ts similarity index 100% rename from packages/billing-gateway/src/index.ts rename to packages/billing/gateway/src/index.ts diff --git a/packages/billing-gateway/src/server/services/billing-event-handler/billing-event-handler.service.ts b/packages/billing/gateway/src/server/services/billing-event-handler/billing-event-handler.service.ts similarity index 100% rename from packages/billing-gateway/src/server/services/billing-event-handler/billing-event-handler.service.ts rename to packages/billing/gateway/src/server/services/billing-event-handler/billing-event-handler.service.ts diff --git a/packages/billing-gateway/src/server/services/billing-event-handler/billing-gateway-factory.service.ts b/packages/billing/gateway/src/server/services/billing-event-handler/billing-gateway-factory.service.ts similarity index 100% rename from packages/billing-gateway/src/server/services/billing-event-handler/billing-gateway-factory.service.ts rename to packages/billing/gateway/src/server/services/billing-event-handler/billing-gateway-factory.service.ts diff --git a/packages/billing-gateway/src/server/services/billing-event-handler/billing-gateway-provider-factory.ts b/packages/billing/gateway/src/server/services/billing-event-handler/billing-gateway-provider-factory.ts similarity index 100% rename from packages/billing-gateway/src/server/services/billing-event-handler/billing-gateway-provider-factory.ts rename to packages/billing/gateway/src/server/services/billing-event-handler/billing-gateway-provider-factory.ts diff --git a/packages/billing-gateway/src/server/services/billing-gateway/billing-gateway-factory.service.ts b/packages/billing/gateway/src/server/services/billing-gateway/billing-gateway-factory.service.ts similarity index 80% rename from packages/billing-gateway/src/server/services/billing-gateway/billing-gateway-factory.service.ts rename to packages/billing/gateway/src/server/services/billing-gateway/billing-gateway-factory.service.ts index 63352bbf9..1c4c5d2cc 100644 --- a/packages/billing-gateway/src/server/services/billing-gateway/billing-gateway-factory.service.ts +++ b/packages/billing/gateway/src/server/services/billing-gateway/billing-gateway-factory.service.ts @@ -16,12 +16,16 @@ export class BillingGatewayFactoryService { return new StripeBillingStrategyService(); } - case 'paddle': { - throw new Error('Paddle is not supported yet'); + case 'lemon-squeezy': { + const { LemonSqueezyBillingStrategyService } = await import( + '@kit/lemon-squeezy' + ); + + return new LemonSqueezyBillingStrategyService(); } - case 'lemon-squeezy': { - throw new Error('Lemon Squeezy is not supported yet'); + case 'paddle': { + throw new Error('Paddle is not supported yet'); } default: diff --git a/packages/billing-gateway/src/server/services/billing-gateway/billing-gateway-provider-factory.ts b/packages/billing/gateway/src/server/services/billing-gateway/billing-gateway-provider-factory.ts similarity index 100% rename from packages/billing-gateway/src/server/services/billing-gateway/billing-gateway-provider-factory.ts rename to packages/billing/gateway/src/server/services/billing-gateway/billing-gateway-provider-factory.ts diff --git a/packages/billing-gateway/src/server/services/billing-gateway/billing-gateway.service.ts b/packages/billing/gateway/src/server/services/billing-gateway/billing-gateway.service.ts similarity index 100% rename from packages/billing-gateway/src/server/services/billing-gateway/billing-gateway.service.ts rename to packages/billing/gateway/src/server/services/billing-gateway/billing-gateway.service.ts diff --git a/packages/billing-gateway/src/server/services/billing-webhooks/billing-webhooks.service.ts b/packages/billing/gateway/src/server/services/billing-webhooks/billing-webhooks.service.ts similarity index 100% rename from packages/billing-gateway/src/server/services/billing-webhooks/billing-webhooks.service.ts rename to packages/billing/gateway/src/server/services/billing-webhooks/billing-webhooks.service.ts diff --git a/packages/billing/tsconfig.json b/packages/billing/gateway/tsconfig.json similarity index 100% rename from packages/billing/tsconfig.json rename to packages/billing/gateway/tsconfig.json diff --git a/packages/billing/lemon-squeezy/package.json b/packages/billing/lemon-squeezy/package.json new file mode 100644 index 000000000..5cc2a13f9 --- /dev/null +++ b/packages/billing/lemon-squeezy/package.json @@ -0,0 +1,50 @@ +{ + "name": "@kit/lemon-squeezy", + "private": true, + "version": "0.1.0", + "scripts": { + "clean": "git clean -xdf .turbo node_modules", + "format": "prettier --check \"**/*.{ts,tsx}\"", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "start": "docker run --rm -it --name=stripe -v ~/.config/stripe:/root/.config/stripe stripe/stripe-cli:latest listen --forward-to http://host.docker.internal:3000/api/billing/webhook" + }, + "prettier": "@kit/prettier-config", + "exports": { + ".": "./src/index.ts", + "./components": "./src/components/index.ts" + }, + "peerDependencies": { + "@kit/billing": "0.1.0", + "@kit/shared": "0.1.0", + "@kit/supabase": "0.1.0", + "@kit/ui": "0.1.0" + }, + "dependencies": { + "@lemonsqueezy/lemonsqueezy.js": "2.2.0" + }, + "devDependencies": { + "@kit/billing": "workspace:^", + "@kit/eslint-config": "workspace:*", + "@kit/prettier-config": "workspace:*", + "@kit/shared": "workspace:^", + "@kit/supabase": "workspace:^", + "@kit/tailwind-config": "workspace:*", + "@kit/tsconfig": "workspace:*", + "@kit/ui": "workspace:^" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@kit/eslint-config/base", + "@kit/eslint-config/react" + ] + }, + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + } +} diff --git a/packages/billing/lemon-squeezy/src/index.ts b/packages/billing/lemon-squeezy/src/index.ts new file mode 100644 index 000000000..ec5736c3a --- /dev/null +++ b/packages/billing/lemon-squeezy/src/index.ts @@ -0,0 +1 @@ +export * from './services/lemon-squeezy-billing-strategy.service'; diff --git a/packages/billing/lemon-squeezy/src/schema/lemon-squeezy-server-env.schema.ts b/packages/billing/lemon-squeezy/src/schema/lemon-squeezy-server-env.schema.ts new file mode 100644 index 000000000..3e9440a3e --- /dev/null +++ b/packages/billing/lemon-squeezy/src/schema/lemon-squeezy-server-env.schema.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const getLemonSqueezyEnv = () => + z + .object({ + secretKey: z.string().min(1), + webhooksSecret: z.string().min(1), + storeId: z.number().positive(), + }) + .parse({ + secretKey: process.env.LEMON_SQUEEZY_SECRET_KEY, + webhooksSecret: process.env.LEMON_SQUEEZY_WEBHOOK_SECRET, + storeId: process.env.LEMON_SQUEEZY_STORE_ID, + }); diff --git a/packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-billing-portal-session.ts b/packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-billing-portal-session.ts new file mode 100644 index 000000000..026f06c9a --- /dev/null +++ b/packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-billing-portal-session.ts @@ -0,0 +1,27 @@ +import { getCustomer } from '@lemonsqueezy/lemonsqueezy.js'; +import { z } from 'zod'; + +import { CreateBillingPortalSessionSchema } from '@kit/billing/schema'; + +import { initializeLemonSqueezyClient } from './lemon-squeezy-sdk'; + +/** + * Creates a LemonSqueezy billing portal session for the given parameters. + * + * @param {object} params - The parameters required to create the billing portal session. + * @return {Promise} - A promise that resolves to the URL of the customer portal. + * @throws {Error} - If no customer is found with the given customerId. + */ +export async function createLemonSqueezyBillingPortalSession( + params: z.infer, +) { + await initializeLemonSqueezyClient(); + + const customer = await getCustomer(params.customerId); + + if (!customer?.data) { + throw new Error('No customer found'); + } + + return customer.data.data.attributes.urls.customer_portal; +} diff --git a/packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-checkout.ts b/packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-checkout.ts new file mode 100644 index 000000000..a553f634b --- /dev/null +++ b/packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-checkout.ts @@ -0,0 +1,82 @@ +import { + NewCheckout, + createCheckout, + getCustomer, +} from '@lemonsqueezy/lemonsqueezy.js'; +import { z } from 'zod'; + +import { CreateBillingCheckoutSchema } from '@kit/billing/schema'; + +import { getLemonSqueezyEnv } from '../schema/lemon-squeezy-server-env.schema'; +import { initializeLemonSqueezyClient } from './lemon-squeezy-sdk'; + +/** + * Creates a checkout for a Lemon Squeezy product. + * + * @param {object} params - The parameters for creating the checkout. + * @return {Promise} - A promise that resolves to the created Lemon Squeezy checkout. + * @throws {Error} - If no line items are found in the subscription. + */ +export async function createLemonSqueezyCheckout( + params: z.infer, +) { + await initializeLemonSqueezyClient(); + + const lineItem = params.plan.lineItems[0]; + + if (!lineItem) { + throw new Error('No line items found in subscription'); + } + + const env = getLemonSqueezyEnv(); + const storeId = env.storeId; + const variantId = lineItem.id; + + const urls = getUrls({ + returnUrl: params.returnUrl, + }); + + const customer = params.customerId + ? await getCustomer(params.customerId) + : null; + + let customerEmail = params.customerEmail; + + // if we can find an existing customer using the ID, + // we use the email from the customer object so that we can + // link the previous subscription to this one + // otherwise it will create a new customer if another email is provided (ex. a different team member) + if (customer?.data) { + customerEmail = customer.data.data.attributes.email; + } + + const newCheckout: NewCheckout = { + checkoutOptions: { + embed: true, + media: true, + logo: true, + }, + checkoutData: { + email: customerEmail, + custom: { + account_id: params.accountId, + }, + }, + productOptions: { + redirectUrl: urls.return_url, + }, + expiresAt: null, + preview: true, + testMode: process.env.NODE_ENV !== 'production', + }; + + return createCheckout(storeId, variantId, newCheckout); +} + +function getUrls(params: { returnUrl: string }) { + const returnUrl = `${params.returnUrl}?session_id={CHECKOUT_SESSION_ID}`; + + return { + return_url: returnUrl, + }; +} diff --git a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts new file mode 100644 index 000000000..ca0f4bd33 --- /dev/null +++ b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts @@ -0,0 +1,89 @@ +import { + cancelSubscription, + createUsageRecord, + getCheckout, +} from '@lemonsqueezy/lemonsqueezy.js'; +import 'server-only'; +import { z } from 'zod'; + +import { BillingStrategyProviderService } from '@kit/billing'; +import { + CancelSubscriptionParamsSchema, + CreateBillingCheckoutSchema, + CreateBillingPortalSessionSchema, + ReportBillingUsageSchema, + RetrieveCheckoutSessionSchema, +} from '@kit/billing/schema'; + +import { createLemonSqueezyBillingPortalSession } from './create-lemon-squeezy-billing-portal-session'; +import { createLemonSqueezyCheckout } from './create-lemon-squeezy-checkout'; + +export class LemonSqueezyBillingStrategyService + implements BillingStrategyProviderService +{ + async createCheckoutSession( + params: z.infer, + ) { + const { data: response } = await createLemonSqueezyCheckout(params); + + if (!response?.data.id) { + throw new Error('Failed to create checkout session'); + } + + return { checkoutToken: response.data.id }; + } + + async createBillingPortalSession( + params: z.infer, + ) { + const url = await createLemonSqueezyBillingPortalSession(params); + + if (!url) { + throw new Error('Failed to create billing portal session'); + } + + return { url }; + } + + async cancelSubscription( + params: z.infer, + ) { + await cancelSubscription(params.subscriptionId); + + return { success: true }; + } + + async retrieveCheckoutSession( + params: z.infer, + ) { + const session = await getCheckout(params.sessionId); + + if (!session.data) { + throw new Error('Failed to retrieve checkout session'); + } + + const data = session.data.data; + + return { + checkoutToken: data.id, + isSessionOpen: false, + status: 'complete' as const, + customer: { + email: data.attributes.checkout_data.email, + }, + }; + } + + async reportUsage(params: z.infer) { + const { error } = await createUsageRecord({ + quantity: params.usage.quantity, + subscriptionItemId: params.subscriptionId, + }); + + if (error) { + throw new Error('Failed to report usage'); + } + + return { success: true }; + } +} diff --git a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-sdk.ts b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-sdk.ts new file mode 100644 index 000000000..e499c7c92 --- /dev/null +++ b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-sdk.ts @@ -0,0 +1,26 @@ +import 'server-only'; + +import { Logger } from '@kit/shared/logger'; + +import { getLemonSqueezyEnv } from '../schema/lemon-squeezy-server-env.schema'; + +/** + * @description Initialize the Lemon Squeezy client + */ +export async function initializeLemonSqueezyClient() { + const { lemonSqueezySetup } = await import('@lemonsqueezy/lemonsqueezy.js'); + const env = getLemonSqueezyEnv(); + + lemonSqueezySetup({ + apiKey: env.secretKey, + onError(error) { + Logger.error( + { + name: `billing.lemon-squeezy`, + error: error.message, + }, + 'Error in Lemon Squeezy SDK', + ); + }, + }); +} diff --git a/packages/stripe/tsconfig.json b/packages/billing/lemon-squeezy/tsconfig.json similarity index 100% rename from packages/stripe/tsconfig.json rename to packages/billing/lemon-squeezy/tsconfig.json diff --git a/packages/stripe/package.json b/packages/billing/stripe/package.json similarity index 100% rename from packages/stripe/package.json rename to packages/billing/stripe/package.json diff --git a/packages/stripe/src/components/index.ts b/packages/billing/stripe/src/components/index.ts similarity index 100% rename from packages/stripe/src/components/index.ts rename to packages/billing/stripe/src/components/index.ts diff --git a/packages/stripe/src/components/stripe-embedded-checkout.tsx b/packages/billing/stripe/src/components/stripe-embedded-checkout.tsx similarity index 100% rename from packages/stripe/src/components/stripe-embedded-checkout.tsx rename to packages/billing/stripe/src/components/stripe-embedded-checkout.tsx diff --git a/packages/stripe/src/index.ts b/packages/billing/stripe/src/index.ts similarity index 100% rename from packages/stripe/src/index.ts rename to packages/billing/stripe/src/index.ts diff --git a/packages/stripe/src/schema/stripe-client-env.schema.ts b/packages/billing/stripe/src/schema/stripe-client-env.schema.ts similarity index 100% rename from packages/stripe/src/schema/stripe-client-env.schema.ts rename to packages/billing/stripe/src/schema/stripe-client-env.schema.ts diff --git a/packages/stripe/src/schema/stripe-server-env.schema.ts b/packages/billing/stripe/src/schema/stripe-server-env.schema.ts similarity index 100% rename from packages/stripe/src/schema/stripe-server-env.schema.ts rename to packages/billing/stripe/src/schema/stripe-server-env.schema.ts diff --git a/packages/stripe/src/services/create-stripe-billing-portal-session.ts b/packages/billing/stripe/src/services/create-stripe-billing-portal-session.ts similarity index 100% rename from packages/stripe/src/services/create-stripe-billing-portal-session.ts rename to packages/billing/stripe/src/services/create-stripe-billing-portal-session.ts diff --git a/packages/stripe/src/services/create-stripe-checkout.ts b/packages/billing/stripe/src/services/create-stripe-checkout.ts similarity index 100% rename from packages/stripe/src/services/create-stripe-checkout.ts rename to packages/billing/stripe/src/services/create-stripe-checkout.ts diff --git a/packages/stripe/src/services/stripe-billing-strategy.service.ts b/packages/billing/stripe/src/services/stripe-billing-strategy.service.ts similarity index 100% rename from packages/stripe/src/services/stripe-billing-strategy.service.ts rename to packages/billing/stripe/src/services/stripe-billing-strategy.service.ts diff --git a/packages/stripe/src/services/stripe-sdk.ts b/packages/billing/stripe/src/services/stripe-sdk.ts similarity index 100% rename from packages/stripe/src/services/stripe-sdk.ts rename to packages/billing/stripe/src/services/stripe-sdk.ts diff --git a/packages/stripe/src/services/stripe-webhook-handler.service.ts b/packages/billing/stripe/src/services/stripe-webhook-handler.service.ts similarity index 100% rename from packages/stripe/src/services/stripe-webhook-handler.service.ts rename to packages/billing/stripe/src/services/stripe-webhook-handler.service.ts diff --git a/packages/billing/stripe/tsconfig.json b/packages/billing/stripe/tsconfig.json new file mode 100644 index 000000000..c4697e934 --- /dev/null +++ b/packages/billing/stripe/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@kit/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/packages/stripe/src/types/stripe-webhooks.enum.ts b/packages/stripe/src/types/stripe-webhooks.enum.ts deleted file mode 100644 index 898cfaf0c..000000000 --- a/packages/stripe/src/types/stripe-webhooks.enum.ts +++ /dev/null @@ -1,9 +0,0 @@ -enum StripeWebhooks { - AsyncPaymentSuccess = 'checkout.session.async_payment_succeeded', - Completed = 'checkout.session.completed', - AsyncPaymentFailed = 'checkout.session.async_payment_failed', - SubscriptionDeleted = 'customer.subscription.deleted', - SubscriptionUpdated = 'customer.subscription.updated', -} - -export default StripeWebhooks; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f43ddb9b4..9ce2ecf32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,10 +49,10 @@ importers: version: link:../../packages/features/auth '@kit/billing': specifier: workspace:^ - version: link:../../packages/billing + version: link:../../packages/billing/core '@kit/billing-gateway': specifier: workspace:^ - version: link:../../packages/billing-gateway + version: link:../../packages/billing/gateway '@kit/cms': specifier: workspace:^ version: link:../../packages/cms/core @@ -193,26 +193,26 @@ importers: specifier: ^5.4.3 version: 5.4.3 - packages/billing: + packages/billing/core: devDependencies: '@kit/eslint-config': specifier: workspace:* - version: link:../../tooling/eslint + version: link:../../../tooling/eslint '@kit/prettier-config': specifier: workspace:* - version: link:../../tooling/prettier + version: link:../../../tooling/prettier '@kit/supabase': specifier: workspace:* - version: link:../supabase + version: link:../../supabase '@kit/tailwind-config': specifier: workspace:* - version: link:../../tooling/tailwind + version: link:../../../tooling/tailwind '@kit/tsconfig': specifier: workspace:* - version: link:../../tooling/typescript + version: link:../../../tooling/typescript '@kit/ui': specifier: workspace:* - version: link:../ui + version: link:../../ui lucide-react: specifier: ^0.363.0 version: 0.363.0(react@18.2.0) @@ -220,35 +220,38 @@ importers: specifier: ^3.22.4 version: 3.22.4 - packages/billing-gateway: + packages/billing/gateway: devDependencies: '@kit/billing': specifier: workspace:^ - version: link:../billing + version: link:../core '@kit/eslint-config': specifier: workspace:* - version: link:../../tooling/eslint + version: link:../../../tooling/eslint + '@kit/lemon-squeezy': + specifier: workspace:^ + version: link:../lemon-squeezy '@kit/prettier-config': specifier: workspace:* - version: link:../../tooling/prettier + version: link:../../../tooling/prettier '@kit/shared': specifier: workspace:^ - version: link:../shared + version: link:../../shared '@kit/stripe': specifier: workspace:^ version: link:../stripe '@kit/supabase': specifier: workspace:^ - version: link:../supabase + version: link:../../supabase '@kit/tailwind-config': specifier: workspace:* - version: link:../../tooling/tailwind + version: link:../../../tooling/tailwind '@kit/tsconfig': specifier: workspace:* - version: link:../../tooling/typescript + version: link:../../../tooling/typescript '@kit/ui': specifier: workspace:^ - version: link:../ui + version: link:../../ui '@supabase/supabase-js': specifier: ^2.41.1 version: 2.41.1 @@ -259,6 +262,74 @@ importers: specifier: ^3.22.4 version: 3.22.4 + packages/billing/lemon-squeezy: + dependencies: + '@lemonsqueezy/lemonsqueezy.js': + specifier: 2.2.0 + version: 2.2.0 + devDependencies: + '@kit/billing': + specifier: workspace:^ + version: link:../core + '@kit/eslint-config': + specifier: workspace:* + version: link:../../../tooling/eslint + '@kit/prettier-config': + specifier: workspace:* + version: link:../../../tooling/prettier + '@kit/shared': + specifier: workspace:^ + version: link:../../shared + '@kit/supabase': + specifier: workspace:^ + version: link:../../supabase + '@kit/tailwind-config': + specifier: workspace:* + version: link:../../../tooling/tailwind + '@kit/tsconfig': + specifier: workspace:* + version: link:../../../tooling/typescript + '@kit/ui': + specifier: workspace:^ + version: link:../../ui + + packages/billing/stripe: + dependencies: + '@stripe/react-stripe-js': + specifier: ^2.6.2 + version: 2.6.2(@stripe/stripe-js@3.1.0)(react-dom@18.2.0)(react@18.2.0) + '@stripe/stripe-js': + specifier: ^3.1.0 + version: 3.1.0 + stripe: + specifier: ^14.22.0 + version: 14.22.0 + devDependencies: + '@kit/billing': + specifier: workspace:^ + version: link:../core + '@kit/eslint-config': + specifier: workspace:* + version: link:../../../tooling/eslint + '@kit/prettier-config': + specifier: workspace:* + version: link:../../../tooling/prettier + '@kit/shared': + specifier: workspace:^ + version: link:../../shared + '@kit/supabase': + specifier: workspace:^ + version: link:../../supabase + '@kit/tailwind-config': + specifier: workspace:* + version: link:../../../tooling/tailwind + '@kit/tsconfig': + specifier: workspace:* + version: link:../../../tooling/typescript + '@kit/ui': + specifier: workspace:^ + version: link:../../ui + packages/cms/contentlayer: dependencies: '@kit/cms': @@ -335,14 +406,14 @@ importers: dependencies: '@kit/billing-gateway': specifier: workspace:^ - version: link:../billing-gateway + version: link:../billing/gateway '@kit/team-accounts': specifier: workspace:^ version: link:../features/team-accounts devDependencies: '@kit/billing': specifier: workspace:^ - version: link:../billing + version: link:../billing/core '@kit/eslint-config': specifier: workspace:* version: link:../../tooling/eslint @@ -354,7 +425,7 @@ importers: version: link:../shared '@kit/stripe': specifier: workspace:^ - version: link:../stripe + version: link:../billing/stripe '@kit/supabase': specifier: workspace:^ version: link:../supabase @@ -403,7 +474,7 @@ importers: version: 3.3.4(react-hook-form@7.51.2) '@kit/billing-gateway': specifier: workspace:^ - version: link:../../billing-gateway + version: link:../../billing/gateway '@kit/email-templates': specifier: workspace:^ version: link:../../email-templates @@ -535,7 +606,7 @@ importers: version: link:../accounts '@kit/billing-gateway': specifier: workspace:* - version: link:../../billing-gateway + version: link:../../billing/gateway '@kit/email-templates': specifier: workspace:^ version: link:../../email-templates @@ -644,43 +715,6 @@ importers: specifier: workspace:* version: link:../../tooling/typescript - packages/stripe: - dependencies: - '@stripe/react-stripe-js': - specifier: ^2.6.2 - version: 2.6.2(@stripe/stripe-js@3.1.0)(react-dom@18.2.0)(react@18.2.0) - '@stripe/stripe-js': - specifier: ^3.1.0 - version: 3.1.0 - stripe: - specifier: ^14.22.0 - version: 14.22.0 - devDependencies: - '@kit/billing': - specifier: workspace:^ - version: link:../billing - '@kit/eslint-config': - specifier: workspace:* - version: link:../../tooling/eslint - '@kit/prettier-config': - specifier: workspace:* - version: link:../../tooling/prettier - '@kit/shared': - specifier: workspace:^ - version: link:../shared - '@kit/supabase': - specifier: workspace:^ - version: link:../supabase - '@kit/tailwind-config': - specifier: workspace:* - version: link:../../tooling/tailwind - '@kit/tsconfig': - specifier: workspace:* - version: link:../../tooling/typescript - '@kit/ui': - specifier: workspace:^ - version: link:../ui - packages/supabase: devDependencies: '@epic-web/invariant': @@ -2036,6 +2070,11 @@ packages: tslib: 2.6.2 dev: false + /@lemonsqueezy/lemonsqueezy.js@2.2.0: + resolution: {integrity: sha512-DsZTeowehSLTESUZ6xxoYPDhoE8BYepWsj3TCqibG7FvB8X1HERPXQlc6E/IeGj22SOfIM997b7GfFkeLWY8pA==} + engines: {node: '>=18'} + dev: false + /@manypkg/cli@0.21.3: resolution: {integrity: sha512-ro6j5b+44dN2AfId23voWxdlOqUCSbCwUHrUwq0LpoN/oZy6zQFAHDwYHbw50j2nL9EgpwIA03ZjaBceuUcMrw==} engines: {node: '>=14.18.0'} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 964b1f635..09130ad89 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - apps/* - packages/* - packages/features/* + - packages/billing/* - packages/cms/* - tooling/* - supabase \ No newline at end of file