feat: open billing for lmz customers #538
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@carbon/charts-svelte": "^1.13.6",
|
"@carbon/charts-svelte": "^1.13.6",
|
||||||
|
"@lemonsqueezy/lemonsqueezy.js": "^4.0.0",
|
||||||
"@polar-sh/sveltekit": "^0.2.1",
|
"@polar-sh/sveltekit": "^0.2.1",
|
||||||
"@sentry/browser": "^7.61.0",
|
"@sentry/browser": "^7.61.0",
|
||||||
"@sentry/integrations": "^7.61.0",
|
"@sentry/integrations": "^7.61.0",
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ const PUBLIC_ROUTES = [
|
|||||||
'/api/completion',
|
'/api/completion',
|
||||||
'student_prove_payment',
|
'student_prove_payment',
|
||||||
'teacher_student_buycourse',
|
'teacher_student_buycourse',
|
||||||
'/api/polar'
|
'/api/polar',
|
||||||
|
'/api/lmz'
|
||||||
];
|
];
|
||||||
|
|
||||||
function isPublicRoute(pathname: string) {
|
function isPublicRoute(pathname: string) {
|
||||||
|
|||||||
@@ -1,21 +1,44 @@
|
|||||||
<script>
|
<script>
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
|
import { snackbar } from '$lib/components/Snackbar/store';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import { currentOrgPlan, isFreePlan } from '$lib/utils/store/org';
|
import { currentOrgPlan, isFreePlan } from '$lib/utils/store/org';
|
||||||
import { Column, Grid, Row } from 'carbon-components-svelte';
|
import { Column, Grid, Row } from 'carbon-components-svelte';
|
||||||
import SectionTitle from '../SectionTitle.svelte';
|
import SectionTitle from '../SectionTitle.svelte';
|
||||||
|
|
||||||
|
let isRedirecting = false;
|
||||||
|
|
||||||
async function onOpenBilling() {
|
async function onOpenBilling() {
|
||||||
console.log({
|
console.log({
|
||||||
plan: $currentOrgPlan
|
plan: $currentOrgPlan
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($currentOrgPlan?.customerId) {
|
try {
|
||||||
const url = new URL('/api/polar/portal', window.location.origin);
|
if ($currentOrgPlan?.provider === 'polar') {
|
||||||
url.searchParams.set('customerId', $currentOrgPlan?.customerId);
|
const url = new URL('/api/polar/portal', window.location.origin);
|
||||||
|
url.searchParams.set('customerId', $currentOrgPlan?.customerId);
|
||||||
|
|
||||||
window.open(url.toString(), '_blank');
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentOrgPlan?.provider === 'lmz') {
|
||||||
|
isRedirecting = true;
|
||||||
|
const url = new URL('/api/lmz/subscription', window.location.origin);
|
||||||
|
url.searchParams.set('subscriptionId', $currentOrgPlan?.subscriptionId);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
isRedirecting = false;
|
||||||
|
|
||||||
|
window.open(result.customerPortal, '_blank');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
isRedirecting = false;
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
snackbar.error($t('settings.billing.error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -36,7 +59,7 @@
|
|||||||
label={$t('settings.billing.open_billing')}
|
label={$t('settings.billing.open_billing')}
|
||||||
variant={VARIANTS.CONTAINED_DARK}
|
variant={VARIANTS.CONTAINED_DARK}
|
||||||
onClick={onOpenBilling}
|
onClick={onOpenBilling}
|
||||||
isDisabled={!$currentOrgPlan?.customerId}
|
isLoading={isRedirecting}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Column>
|
</Column>
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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<SubscriptionResponse> {
|
||||||
|
const accessToken = await getAccessToken();
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/lmz/subscribe',
|
||||||
|
data,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
@@ -77,6 +77,8 @@ export async function getOrganizations(userId: string, isOrgSite?: boolean, orgS
|
|||||||
organization_plan(
|
organization_plan(
|
||||||
plan_name,
|
plan_name,
|
||||||
is_active,
|
is_active,
|
||||||
|
provider,
|
||||||
|
subscriptionId:payload->id,
|
||||||
customerId:payload->customerId
|
customerId:payload->customerId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1148,7 +1148,8 @@
|
|||||||
"manage": "Verwalten Sie Ihre Abrechnung",
|
"manage": "Verwalten Sie Ihre Abrechnung",
|
||||||
"active": "Sie haben keine aktive Abrechnung",
|
"active": "Sie haben keine aktive Abrechnung",
|
||||||
"open_billing": "Abrechnung öffnen",
|
"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": {
|
"integrations": {
|
||||||
"heading": "Telegramm",
|
"heading": "Telegramm",
|
||||||
|
|||||||
@@ -243,7 +243,8 @@
|
|||||||
"manage": "Manage your billing",
|
"manage": "Manage your billing",
|
||||||
"active": "You don't have any active billing",
|
"active": "You don't have any active billing",
|
||||||
"provider": "We use Polar to help manage your 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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1148,7 +1148,8 @@
|
|||||||
"manage": "Gestiona tu facturación",
|
"manage": "Gestiona tu facturación",
|
||||||
"active": "No tienes ninguna facturación activa",
|
"active": "No tienes ninguna facturación activa",
|
||||||
"open_billing": "Facturación abierta",
|
"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": {
|
"integrations": {
|
||||||
"heading": "Telegrama",
|
"heading": "Telegrama",
|
||||||
|
|||||||
@@ -1148,7 +1148,8 @@
|
|||||||
"manage": "Gérez votre facturation",
|
"manage": "Gérez votre facturation",
|
||||||
"active": "Vous n'avez aucune facturation active",
|
"active": "Vous n'avez aucune facturation active",
|
||||||
"open_billing": "Facturation ouverte",
|
"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": {
|
"integrations": {
|
||||||
"heading": "Télégramme",
|
"heading": "Télégramme",
|
||||||
|
|||||||
@@ -1148,7 +1148,8 @@
|
|||||||
"manage": "अपनी बिलिंग प्रबंधित करें",
|
"manage": "अपनी बिलिंग प्रबंधित करें",
|
||||||
"active": "आपके पास कोई सक्रिय बिलिंग नहीं है",
|
"active": "आपके पास कोई सक्रिय बिलिंग नहीं है",
|
||||||
"open_billing": "बिलिंग खोलें",
|
"open_billing": "बिलिंग खोलें",
|
||||||
"provider": "हम आपकी बिलिंग प्रबंधित करने में सहायता के लिए पोलर का उपयोग करते हैं"
|
"provider": "हम आपकी बिलिंग प्रबंधित करने में सहायता के लिए पोलर का उपयोग करते हैं",
|
||||||
|
"error": "बिलिंग खोलते समय एक त्रुटि उत्पन्न हुई"
|
||||||
},
|
},
|
||||||
"integrations": {
|
"integrations": {
|
||||||
"heading": "तार",
|
"heading": "तार",
|
||||||
|
|||||||
@@ -1148,7 +1148,8 @@
|
|||||||
"manage": "Gerencie seu faturamento",
|
"manage": "Gerencie seu faturamento",
|
||||||
"active": "Você não tem nenhum faturamento ativo",
|
"active": "Você não tem nenhum faturamento ativo",
|
||||||
"open_billing": "Faturamento aberto",
|
"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": {
|
"integrations": {
|
||||||
"heading": "Telegrama",
|
"heading": "Telegrama",
|
||||||
|
|||||||
@@ -1148,7 +1148,8 @@
|
|||||||
"manage": "Управляйте своим биллингом",
|
"manage": "Управляйте своим биллингом",
|
||||||
"active": "У вас нет активных платежных данных.",
|
"active": "У вас нет активных платежных данных.",
|
||||||
"open_billing": "Открыть биллинг",
|
"open_billing": "Открыть биллинг",
|
||||||
"provider": "Мы используем Polar, чтобы управлять вашими платежами."
|
"provider": "Мы используем Polar, чтобы управлять вашими платежами.",
|
||||||
|
"error": "Произошла ошибка при открытии счета."
|
||||||
},
|
},
|
||||||
"integrations": {
|
"integrations": {
|
||||||
"heading": "Телеграмма",
|
"heading": "Телеграмма",
|
||||||
|
|||||||
@@ -1148,7 +1148,8 @@
|
|||||||
"manage": "Quản lý thanh toán của bạn",
|
"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",
|
"active": "Bạn không có bất kỳ thanh toán nào đang hoạt động",
|
||||||
"open_billing": "Thanh toán mở",
|
"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": {
|
"integrations": {
|
||||||
"heading": "điện tín",
|
"heading": "điện tín",
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export interface CurrentOrg {
|
|||||||
customization: OrgCustomization;
|
customization: OrgCustomization;
|
||||||
theme: string;
|
theme: string;
|
||||||
organization_plan: {
|
organization_plan: {
|
||||||
|
provider: 'polar' | 'lmz';
|
||||||
|
subscriptionId: string;
|
||||||
customerId: string;
|
customerId: string;
|
||||||
plan_name: string;
|
plan_name: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ async function getOrganisationAnalytics(orgId: string): Promise<OrganisationAnal
|
|||||||
};
|
};
|
||||||
// Run all queries in parallel
|
// Run all queries in parallel
|
||||||
const [statsResult, topCoursesResult, enrollmentsResult] = await Promise.all([
|
const [statsResult, topCoursesResult, enrollmentsResult] = await Promise.all([
|
||||||
supabase.from('dash_org_stats').select('*').eq('org_id', orgId).single(),
|
supabase.from('dash_org_stats').select('*').eq('org_id', orgId),
|
||||||
|
|
||||||
supabase
|
supabase
|
||||||
.rpc('get_dash_org_top_courses', {
|
.rpc('get_dash_org_top_courses', {
|
||||||
@@ -66,8 +66,8 @@ async function getOrganisationAnalytics(orgId: string): Promise<OrganisationAnal
|
|||||||
|
|
||||||
// Set analytics data
|
// Set analytics data
|
||||||
analytics.revenue = 0; // Hardcode until we implement payment
|
analytics.revenue = 0; // Hardcode until we implement payment
|
||||||
analytics.numberOfCourses = statsResult.data.no_of_courses;
|
analytics.numberOfCourses = statsResult.data?.[0]?.no_of_courses;
|
||||||
analytics.totalStudents = statsResult.data.enrolled_students;
|
analytics.totalStudents = statsResult.data?.[0]?.enrolled_students;
|
||||||
|
|
||||||
analytics.topCourses = topCoursesResult.data.map((course) => ({
|
analytics.topCourses = topCoursesResult.data.map((course) => ({
|
||||||
id: course.course_id,
|
id: course.course_id,
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
Generated
+11
-6
@@ -266,6 +266,9 @@ importers:
|
|||||||
'@carbon/charts-svelte':
|
'@carbon/charts-svelte':
|
||||||
specifier: ^1.13.6
|
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)
|
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':
|
'@polar-sh/sveltekit':
|
||||||
specifier: ^0.2.1
|
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)
|
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':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
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':
|
'@mapbox/node-pre-gyp@1.0.11':
|
||||||
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
|
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -10508,7 +10515,7 @@ snapshots:
|
|||||||
'@babel/traverse': 7.23.4
|
'@babel/traverse': 7.23.4
|
||||||
'@babel/types': 7.23.4
|
'@babel/types': 7.23.4
|
||||||
convert-source-map: 1.9.0
|
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
|
gensync: 1.0.0-beta.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@@ -10528,7 +10535,7 @@ snapshots:
|
|||||||
'@babel/traverse': 7.23.4
|
'@babel/traverse': 7.23.4
|
||||||
'@babel/types': 7.23.4
|
'@babel/types': 7.23.4
|
||||||
convert-source-map: 2.0.0
|
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
|
gensync: 1.0.0-beta.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@@ -12248,6 +12255,8 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
|
'@lemonsqueezy/lemonsqueezy.js@4.0.0': {}
|
||||||
|
|
||||||
'@mapbox/node-pre-gyp@1.0.11':
|
'@mapbox/node-pre-gyp@1.0.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-libc: 2.0.2
|
detect-libc: 2.0.2
|
||||||
@@ -15321,10 +15330,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
debug@4.3.4:
|
|
||||||
dependencies:
|
|
||||||
ms: 2.1.2
|
|
||||||
|
|
||||||
debug@4.3.4(supports-color@5.5.0):
|
debug@4.3.4(supports-color@5.5.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
|
|||||||
Reference in New Issue
Block a user