From 6ab64a26e58cab2c1d9e9c4b7d65273f4fda6c95 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Fri, 10 Jan 2025 07:53:00 +0100 Subject: [PATCH] feat: open billing for lmz customers #538 --- apps/dashboard/package.json | 1 + apps/dashboard/src/hooks.server.ts | 3 +- .../components/Org/Settings/Billing.svelte | 33 ++++++++++++++++--- .../lib/utils/services/lemonsqueezy/index.ts | 11 +++++++ .../utils/services/lemonsqueezy/subscribe.ts | 30 +++++++++++++++++ .../src/lib/utils/services/org/index.ts | 2 ++ .../src/lib/utils/translations/de.json | 3 +- .../src/lib/utils/translations/en.json | 3 +- .../src/lib/utils/translations/es.json | 3 +- .../src/lib/utils/translations/fr.json | 3 +- .../src/lib/utils/translations/hi.json | 3 +- .../src/lib/utils/translations/pt.json | 3 +- .../src/lib/utils/translations/ru.json | 3 +- .../src/lib/utils/translations/vi.json | 3 +- apps/dashboard/src/lib/utils/types/org.ts | 2 ++ .../src/routes/api/analytics/dash/+server.ts | 6 ++-- .../routes/api/lmz/subscription/+server.ts | 23 +++++++++++++ pnpm-lock.yaml | 17 ++++++---- 18 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 apps/dashboard/src/lib/utils/services/lemonsqueezy/index.ts create mode 100644 apps/dashboard/src/lib/utils/services/lemonsqueezy/subscribe.ts create mode 100644 apps/dashboard/src/routes/api/lmz/subscription/+server.ts diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 23609e33..d200149f 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@carbon/charts-svelte": "^1.13.6", + "@lemonsqueezy/lemonsqueezy.js": "^4.0.0", "@polar-sh/sveltekit": "^0.2.1", "@sentry/browser": "^7.61.0", "@sentry/integrations": "^7.61.0", diff --git a/apps/dashboard/src/hooks.server.ts b/apps/dashboard/src/hooks.server.ts index d438bdd7..782f55e5 100644 --- a/apps/dashboard/src/hooks.server.ts +++ b/apps/dashboard/src/hooks.server.ts @@ -5,7 +5,8 @@ const PUBLIC_ROUTES = [ '/api/completion', 'student_prove_payment', 'teacher_student_buycourse', - '/api/polar' + '/api/polar', + '/api/lmz' ]; function isPublicRoute(pathname: string) { diff --git a/apps/dashboard/src/lib/components/Org/Settings/Billing.svelte b/apps/dashboard/src/lib/components/Org/Settings/Billing.svelte index 006f68e5..a35db44d 100644 --- a/apps/dashboard/src/lib/components/Org/Settings/Billing.svelte +++ b/apps/dashboard/src/lib/components/Org/Settings/Billing.svelte @@ -1,21 +1,44 @@ @@ -36,7 +59,7 @@ label={$t('settings.billing.open_billing')} variant={VARIANTS.CONTAINED_DARK} onClick={onOpenBilling} - isDisabled={!$currentOrgPlan?.customerId} + isLoading={isRedirecting} /> {/if} diff --git a/apps/dashboard/src/lib/utils/services/lemonsqueezy/index.ts b/apps/dashboard/src/lib/utils/services/lemonsqueezy/index.ts new file mode 100644 index 00000000..358e4d88 --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/lemonsqueezy/index.ts @@ -0,0 +1,11 @@ +import { env } from '$env/dynamic/private'; +import { lemonSqueezySetup } from '@lemonsqueezy/lemonsqueezy.js'; + +export function configureLemonSqueezy() { + return lemonSqueezySetup({ + apiKey: env.LEMON_SQUEEZY_API_KEY, + onError: (error) => { + throw new Error(`Lemon Squeezy API error: ${error.message}`); + } + }); +} diff --git a/apps/dashboard/src/lib/utils/services/lemonsqueezy/subscribe.ts b/apps/dashboard/src/lib/utils/services/lemonsqueezy/subscribe.ts new file mode 100644 index 00000000..59a824c8 --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/lemonsqueezy/subscribe.ts @@ -0,0 +1,30 @@ +import { getAccessToken } from '$lib/utils/functions/supabase'; +import axios from 'axios'; + +interface SubscriptionData { + productId: string; + email: string; + name: string; + triggeredBy: string; + orgId: string; +} + +interface SubscriptionResponse { + success: boolean; + checkoutURL: string; +} + +export async function subscribeToProduct(data: SubscriptionData): Promise { + const accessToken = await getAccessToken(); + + const response = await axios({ + method: 'POST', + url: '/api/lmz/subscribe', + data, + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + return response.data; +} diff --git a/apps/dashboard/src/lib/utils/services/org/index.ts b/apps/dashboard/src/lib/utils/services/org/index.ts index 25901342..32b9b620 100644 --- a/apps/dashboard/src/lib/utils/services/org/index.ts +++ b/apps/dashboard/src/lib/utils/services/org/index.ts @@ -77,6 +77,8 @@ export async function getOrganizations(userId: string, isOrgSite?: boolean, orgS organization_plan( plan_name, is_active, + provider, + subscriptionId:payload->id, customerId:payload->customerId ) ) diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index 69e43ea7..623cd236 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -1148,7 +1148,8 @@ "manage": "Verwalten Sie Ihre Abrechnung", "active": "Sie haben keine aktive Abrechnung", "open_billing": "Abrechnung öffnen", - "provider": "Wir verwenden Polar, um Ihre Abrechnung zu verwalten" + "provider": "Wir verwenden Polar, um Ihre Abrechnung zu verwalten", + "error": "Beim Öffnen der Abrechnung ist ein Fehler aufgetreten" }, "integrations": { "heading": "Telegramm", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index 3088d1f3..a82a9f9e 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -243,7 +243,8 @@ "manage": "Manage your billing", "active": "You don't have any active billing", "provider": "We use Polar to help manage your billing", - "open_billing": "Open billing" + "open_billing": "Open billing", + "error": "An error occurred while opening billing" } }, diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index 6a639a83..3a15e91f 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -1148,7 +1148,8 @@ "manage": "Gestiona tu facturación", "active": "No tienes ninguna facturación activa", "open_billing": "Facturación abierta", - "provider": "Usamos Polar para ayudarte a gestionar tu facturación" + "provider": "Usamos Polar para ayudarte a gestionar tu facturación", + "error": "Se produjo un error al abrir la facturación." }, "integrations": { "heading": "Telegrama", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index 99b920b1..b2e3dafd 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -1148,7 +1148,8 @@ "manage": "Gérez votre facturation", "active": "Vous n'avez aucune facturation active", "open_billing": "Facturation ouverte", - "provider": "Nous utilisons Polar pour vous aider à gérer votre facturation" + "provider": "Nous utilisons Polar pour vous aider à gérer votre facturation", + "error": "Une erreur s'est produite lors de l'ouverture de la facturation" }, "integrations": { "heading": "Télégramme", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index 83d4d4ee..4fc70327 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -1148,7 +1148,8 @@ "manage": "अपनी बिलिंग प्रबंधित करें", "active": "आपके पास कोई सक्रिय बिलिंग नहीं है", "open_billing": "बिलिंग खोलें", - "provider": "हम आपकी बिलिंग प्रबंधित करने में सहायता के लिए पोलर का उपयोग करते हैं" + "provider": "हम आपकी बिलिंग प्रबंधित करने में सहायता के लिए पोलर का उपयोग करते हैं", + "error": "बिलिंग खोलते समय एक त्रुटि उत्पन्न हुई" }, "integrations": { "heading": "तार", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index 65d1bc45..12ed3d16 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -1148,7 +1148,8 @@ "manage": "Gerencie seu faturamento", "active": "Você não tem nenhum faturamento ativo", "open_billing": "Faturamento aberto", - "provider": "Usamos a Polar para ajudar a gerenciar seu faturamento" + "provider": "Usamos a Polar para ajudar a gerenciar seu faturamento", + "error": "Ocorreu um erro ao abrir o faturamento" }, "integrations": { "heading": "Telegrama", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index 18b15a9a..da23c8d9 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -1148,7 +1148,8 @@ "manage": "Управляйте своим биллингом", "active": "У вас нет активных платежных данных.", "open_billing": "Открыть биллинг", - "provider": "Мы используем Polar, чтобы управлять вашими платежами." + "provider": "Мы используем Polar, чтобы управлять вашими платежами.", + "error": "Произошла ошибка при открытии счета." }, "integrations": { "heading": "Телеграмма", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index 0639d8b9..3f77fb58 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -1148,7 +1148,8 @@ "manage": "Quản lý thanh toán của bạn", "active": "Bạn không có bất kỳ thanh toán nào đang hoạt động", "open_billing": "Thanh toán mở", - "provider": "Chúng tôi sử dụng Polar để giúp quản lý việc thanh toán của bạn" + "provider": "Chúng tôi sử dụng Polar để giúp quản lý việc thanh toán của bạn", + "error": "Đã xảy ra lỗi khi mở thanh toán" }, "integrations": { "heading": "điện tín", diff --git a/apps/dashboard/src/lib/utils/types/org.ts b/apps/dashboard/src/lib/utils/types/org.ts index 178c1f02..7f9fa426 100644 --- a/apps/dashboard/src/lib/utils/types/org.ts +++ b/apps/dashboard/src/lib/utils/types/org.ts @@ -29,6 +29,8 @@ export interface CurrentOrg { customization: OrgCustomization; theme: string; organization_plan: { + provider: 'polar' | 'lmz'; + subscriptionId: string; customerId: string; plan_name: string; is_active: boolean; diff --git a/apps/dashboard/src/routes/api/analytics/dash/+server.ts b/apps/dashboard/src/routes/api/analytics/dash/+server.ts index d0adca59..7a024b4d 100644 --- a/apps/dashboard/src/routes/api/analytics/dash/+server.ts +++ b/apps/dashboard/src/routes/api/analytics/dash/+server.ts @@ -37,7 +37,7 @@ async function getOrganisationAnalytics(orgId: string): Promise ({ id: course.course_id, diff --git a/apps/dashboard/src/routes/api/lmz/subscription/+server.ts b/apps/dashboard/src/routes/api/lmz/subscription/+server.ts new file mode 100644 index 00000000..dac1d73a --- /dev/null +++ b/apps/dashboard/src/routes/api/lmz/subscription/+server.ts @@ -0,0 +1,23 @@ +import { configureLemonSqueezy } from '$lib/utils/services/lemonsqueezy'; +import { getSubscription } from '@lemonsqueezy/lemonsqueezy.js'; +import { json } from '@sveltejs/kit'; + +export async function GET({ url }) { + const subscriptionId = url.searchParams.get('subscriptionId'); + + await configureLemonSqueezy(); + + if (!subscriptionId) { + return json({ success: false, message: 'Subscription id is required' }, { status: 400 }); + } + + const response = await getSubscription(subscriptionId); + const customerPortal = response?.data?.data?.attributes?.urls?.customer_portal; + + console.log({ customerPortal }); + + return json({ + success: true, + customerPortal + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ead4040..20091654 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -266,6 +266,9 @@ importers: '@carbon/charts-svelte': specifier: ^1.13.6 version: 1.13.6(d3-cloud@1.2.7)(d3-sankey@0.12.3)(d3@7.8.5)(svelte@4.1.2) + '@lemonsqueezy/lemonsqueezy.js': + specifier: ^4.0.0 + version: 4.0.0 '@polar-sh/sveltekit': specifier: ^0.2.1 version: 0.2.1(@sveltejs/kit@1.29.0(svelte@4.1.2)(vite@4.4.8(@types/node@22.10.1)(sass@1.64.2)))(zod@3.21.4) @@ -2460,6 +2463,10 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@lemonsqueezy/lemonsqueezy.js@4.0.0': + resolution: {integrity: sha512-xcY1/lDrY7CpIF98WKiL1ElsfoVhddP7FT0fw7ssOzrFqQsr44HgolKrQZxd9SywsCPn12OTOUieqDIokI3mFg==} + engines: {node: '>=20'} + '@mapbox/node-pre-gyp@1.0.11': resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true @@ -10508,7 +10515,7 @@ snapshots: '@babel/traverse': 7.23.4 '@babel/types': 7.23.4 convert-source-map: 1.9.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -10528,7 +10535,7 @@ snapshots: '@babel/traverse': 7.23.4 '@babel/types': 7.23.4 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -12248,6 +12255,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@lemonsqueezy/lemonsqueezy.js@4.0.0': {} + '@mapbox/node-pre-gyp@1.0.11': dependencies: detect-libc: 2.0.2 @@ -15321,10 +15330,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.3.4: - dependencies: - ms: 2.1.2 - debug@4.3.4(supports-color@5.5.0): dependencies: ms: 2.1.2