diff --git a/apps/web/app/(dashboard)/home/_components/home-sidebar-account-selector.tsx b/apps/web/app/(dashboard)/home/(user)/_components/home-sidebar-account-selector.tsx similarity index 100% rename from apps/web/app/(dashboard)/home/_components/home-sidebar-account-selector.tsx rename to apps/web/app/(dashboard)/home/(user)/_components/home-sidebar-account-selector.tsx diff --git a/apps/web/app/(dashboard)/home/_components/home-sidebar.tsx b/apps/web/app/(dashboard)/home/(user)/_components/home-sidebar.tsx similarity index 89% rename from apps/web/app/(dashboard)/home/_components/home-sidebar.tsx rename to apps/web/app/(dashboard)/home/(user)/_components/home-sidebar.tsx index 1114e4f23..acf6901fb 100644 --- a/apps/web/app/(dashboard)/home/_components/home-sidebar.tsx +++ b/apps/web/app/(dashboard)/home/(user)/_components/home-sidebar.tsx @@ -5,14 +5,14 @@ import { cookies } from 'next/headers'; import { If } from '@kit/ui/if'; import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar'; +import { loadUserWorkspace } from '~/(dashboard)/home/(user)/_lib/server/load-user-workspace'; import { AppLogo } from '~/components/app-logo'; +import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container'; import featuresFlagConfig from '~/config/feature-flags.config'; import { personalAccountSidebarConfig } from '~/config/personal-account-sidebar.config'; // home imports import { HomeSidebarAccountSelector } from '../_components/home-sidebar-account-selector'; -import { ProfileAccountDropdownContainer } from '../_components/personal-account-dropdown-container'; -import { loadUserWorkspace } from '../_lib/load-user-workspace'; export function HomeSidebar() { const collapsed = getSidebarCollapsed(); diff --git a/apps/web/app/(dashboard)/home/_lib/load-user-workspace.ts b/apps/web/app/(dashboard)/home/(user)/_lib/server/load-user-workspace.ts similarity index 55% rename from apps/web/app/(dashboard)/home/_lib/load-user-workspace.ts rename to apps/web/app/(dashboard)/home/(user)/_lib/server/load-user-workspace.ts index 447ebd508..00c13f912 100644 --- a/apps/web/app/(dashboard)/home/_lib/load-user-workspace.ts +++ b/apps/web/app/(dashboard)/home/(user)/_lib/server/load-user-workspace.ts @@ -1,11 +1,9 @@ import { cache } from 'react'; -import { SupabaseClient } from '@supabase/supabase-js'; - +import { createAccountsApi } from '@kit/accounts/api'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import featureFlagsConfig from '~/config/feature-flags.config'; -import { Database } from '~/lib/database.types'; const shouldLoadAccounts = featureFlagsConfig.enableTeamAccounts; @@ -17,12 +15,13 @@ const shouldLoadAccounts = featureFlagsConfig.enableTeamAccounts; */ export const loadUserWorkspace = cache(async () => { const client = getSupabaseServerComponentClient(); + const api = createAccountsApi(client); const accountsPromise = shouldLoadAccounts - ? () => loadUserAccounts(client) + ? () => api.loadUserAccounts() : () => Promise.resolve([]); - const workspacePromise = loadUserAccountWorkspace(client); + const workspacePromise = api.getAccountWorkspace(); const userPromise = client.auth.getUser(); const [accounts, workspace, userResult] = await Promise.all([ @@ -43,34 +42,3 @@ export const loadUserWorkspace = cache(async () => { user, }; }); - -async function loadUserAccountWorkspace(client: SupabaseClient) { - const { data, error } = await client - .from('user_account_workspace') - .select(`*`) - .single(); - - if (error) { - throw error; - } - - return data; -} - -async function loadUserAccounts(client: SupabaseClient) { - const { data: accounts, error } = await client - .from('user_accounts') - .select(`name, slug, picture_url`); - - if (error) { - throw error; - } - - return accounts.map(({ name, slug, picture_url }) => { - return { - label: name, - value: slug, - image: picture_url, - }; - }); -} diff --git a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts index 82958a77a..15bb17c23 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts +++ b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts @@ -6,10 +6,9 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { z } from 'zod'; +import { createAccountsApi } from '@kit/accounts/api'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; -import { Database } from '~/lib/database.types'; - /** * The variable BILLING_MODE represents the billing mode for a service. It can * have either the value 'subscription' or 'one-time'. If not provided, the default @@ -33,77 +32,14 @@ const BILLING_MODE = z */ export const loadPersonalAccountBillingPageData = cache((userId: string) => { const client = getSupabaseServerComponentClient(); + const api = createAccountsApi(client); const data = BILLING_MODE === 'subscription' - ? getSubscriptionData(client, userId) - : getOrdersData(client, userId); + ? api.getSubscriptionData(userId) + : api.getOrdersData(userId); - const customerId = getBillingCustomerId(client, userId); + const customerId = api.getBillingCustomerId(userId); return Promise.all([data, customerId]); }); - -/** - * Get the subscription data for the given user. - * @param client - * @param userId - */ -function getSubscriptionData(client: SupabaseClient, userId: string) { - return client - .from('subscriptions') - .select('*, items: subscription_items !inner (*)') - .eq('account_id', userId) - .maybeSingle() - .then((response) => { - if (response.error) { - throw response.error; - } - - return response.data; - }); -} - -/** - * Get the orders data for the given user. - * @param client - * @param userId - */ -function getOrdersData(client: SupabaseClient, userId: string) { - return client - .from('orders') - .select('*, items: order_items !inner (*)') - .eq('account_id', userId) - .maybeSingle() - .then((response) => { - if (response.error) { - throw response.error; - } - - return response.data; - }); -} - -/** - * Get the billing customer ID for the given user. - * If the user does not have a billing customer ID, it will return null. - * @param client - * @param userId - */ -function getBillingCustomerId( - client: SupabaseClient, - userId: string, -) { - return client - .from('billing_customers') - .select('customer_id') - .eq('account_id', userId) - .maybeSingle() - .then((response) => { - if (response.error) { - throw response.error; - } - - return response.data?.customer_id; - }); -} diff --git a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts index 9f04525dd..ec52af453 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts +++ b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts @@ -4,11 +4,11 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { z } from 'zod'; +import { createAccountsApi } from '@kit/accounts/api'; import { getProductPlanPair } from '@kit/billing'; import { getBillingGatewayProvider } from '@kit/billing-gateway'; import { getLogger } from '@kit/shared/logger'; import { requireUser } from '@kit/supabase/require-user'; -import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import appConfig from '~/config/app.config'; import billingConfig from '~/config/billing.config'; @@ -22,6 +22,12 @@ export class UserBillingService { constructor(private readonly client: SupabaseClient) {} + /** + * @name createCheckoutSession + * @description Create a checkout session for the user + * @param planId + * @param productId + */ async createCheckoutSession({ planId, productId, @@ -44,7 +50,8 @@ export class UserBillingService { // find the customer ID for the account if it exists // (eg. if the account has been billed before) - const customerId = await getCustomerIdFromAccountId(accountId); + const api = createAccountsApi(this.client); + const customerId = await api.getBillingCustomerId(accountId); const product = billingConfig.products.find( (item) => item.id === productId, @@ -107,6 +114,11 @@ export class UserBillingService { } } + /** + * @name createBillingPortalSession + * @description Create a billing portal session for the user + * @returns The URL to redirect the user to the billing portal + */ async createBillingPortalSession() { const { data, error } = await requireUser(this.client); @@ -118,7 +130,8 @@ export class UserBillingService { const logger = await getLogger(); const accountId = data.id; - const customerId = await getCustomerIdFromAccountId(accountId); + const api = createAccountsApi(this.client); + const customerId = await api.getBillingCustomerId(accountId); const returnUrl = getBillingPortalReturnUrl(); if (!customerId) { @@ -166,38 +179,6 @@ export class UserBillingService { } } -async function getCustomerIdFromAccountId(accountId: string) { - const client = getSupabaseServerActionClient(); - const logger = await getLogger(); - - logger.info( - { - accountId, - }, - `Getting customer ID for account ${accountId}...`, - ); - - const { data, error } = await client - .from('billing_customers') - .select('customer_id') - .eq('account_id', accountId) - .maybeSingle(); - - if (error) { - logger.error( - { - accountId, - error, - }, - `Failed to get customer ID`, - ); - - throw error; - } - - return data?.customer_id; -} - function getCheckoutSessionReturnUrl() { return new URL( pathsConfig.app.personalAccountBillingReturn, diff --git a/apps/web/app/(dashboard)/home/(user)/layout.tsx b/apps/web/app/(dashboard)/home/(user)/layout.tsx index b659d19f7..5c50d9374 100644 --- a/apps/web/app/(dashboard)/home/(user)/layout.tsx +++ b/apps/web/app/(dashboard)/home/(user)/layout.tsx @@ -1,8 +1,9 @@ import { Page } from '@kit/ui/page'; -import { HomeSidebar } from '~/(dashboard)/home/_components/home-sidebar'; import { withI18n } from '~/lib/i18n/with-i18n'; +import { HomeSidebar } from './_components/home-sidebar'; + function UserHomeLayout({ children }: React.PropsWithChildren) { return }>{children}; } diff --git a/apps/web/app/(dashboard)/home/[account]/_components/account-layout-sidebar.tsx b/apps/web/app/(dashboard)/home/[account]/_components/account-layout-sidebar.tsx index d7599f94a..52f3fe51c 100644 --- a/apps/web/app/(dashboard)/home/[account]/_components/account-layout-sidebar.tsx +++ b/apps/web/app/(dashboard)/home/[account]/_components/account-layout-sidebar.tsx @@ -18,7 +18,7 @@ import { import { Trans } from '@kit/ui/trans'; import { cn } from '@kit/ui/utils'; -import { ProfileAccountDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown-container'; +import { ProfileAccountDropdownContainer } from '~/components//personal-account-dropdown-container'; import featureFlagsConfig from '~/config/feature-flags.config'; import pathsConfig from '~/config/paths.config'; diff --git a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts index 8955dfe07..8f691d25c 100644 --- a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts +++ b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts @@ -2,14 +2,11 @@ import 'server-only'; import { cache } from 'react'; -import { SupabaseClient } from '@supabase/supabase-js'; - import { z } from 'zod'; +import { createAccountsApi } from '@kit/accounts/api'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; -import { Database } from '~/lib/database.types'; - /** * The variable BILLING_MODE represents the billing mode for a service. It can * have either the value 'subscription' or 'one-time'. If not provided, the default @@ -27,80 +24,14 @@ const BILLING_MODE = z export const loadTeamAccountBillingPage = cache((accountId: string) => { const client = getSupabaseServerComponentClient(); + const api = createAccountsApi(client); const data = BILLING_MODE === 'subscription' - ? getSubscriptionData(client, accountId) - : getOrdersData(client, accountId); + ? api.getSubscriptionData(accountId) + : api.getOrdersData(accountId); - const customerId = getBillingCustomerId(client, accountId); + const customerId = api.getBillingCustomerId(accountId); return Promise.all([data, customerId]); }); - -/** - * Get the subscription data for the given user. - * @param client - * @param accountId - */ -function getSubscriptionData( - client: SupabaseClient, - accountId: string, -) { - return client - .from('subscriptions') - .select('*, items: subscription_items !inner (*)') - .eq('account_id', accountId) - .maybeSingle() - .then((response) => { - if (response.error) { - throw response.error; - } - - return response.data; - }); -} - -/** - * Get the orders data for the given user. - * @param client - * @param accountId - */ -function getOrdersData(client: SupabaseClient, accountId: string) { - return client - .from('orders') - .select('*, items: order_items !inner (*)') - .eq('account_id', accountId) - .maybeSingle() - .then((response) => { - if (response.error) { - throw response.error; - } - - return response.data; - }); -} - -/** - * Get the billing customer ID for the given user. - * If the user does not have a billing customer ID, it will return null. - * @param client - * @param accountId - */ -function getBillingCustomerId( - client: SupabaseClient, - accountId: string, -) { - return client - .from('billing_customers') - .select('customer_id') - .eq('account_id', accountId) - .maybeSingle() - .then((response) => { - if (response.error) { - throw response.error; - } - - return response.data?.customer_id; - }); -} diff --git a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-workspace.loader.ts b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-workspace.loader.ts index 47902d92c..6a0dd71cd 100644 --- a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-workspace.loader.ts +++ b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-workspace.loader.ts @@ -5,6 +5,7 @@ import { cache } from 'react'; import { redirect } from 'next/navigation'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; +import { createTeamAccountsApi } from '@kit/team-accounts/api'; import pathsConfig from '~/config/paths.config'; @@ -19,50 +20,21 @@ import pathsConfig from '~/config/paths.config'; */ export const loadTeamWorkspace = cache(async (accountSlug: string) => { const client = getSupabaseServerComponentClient(); + const api = createTeamAccountsApi(client); - const accountPromise = client.rpc('team_account_workspace', { - account_slug: accountSlug, - }); + const workspace = await api.getAccountWorkspace(accountSlug); - const accountsPromise = client.from('user_accounts').select('*'); - - const [ - accountResult, - accountsResult, - { - data: { user }, - }, - ] = await Promise.all([ - accountPromise, - accountsPromise, - client.auth.getUser(), - ]); - - if (accountResult.error) { - throw accountResult.error; + if (workspace.error) { + throw workspace.error; } + const account = workspace.data.account; + // we cannot find any record for the selected account // so we redirect the user to the home page - if (!accountResult.data.length) { + if (!account) { return redirect(pathsConfig.app.home); } - const accountData = accountResult.data[0]; - - // we cannot find any record for the selected account - // so we redirect the user to the home page - if (!accountData) { - return redirect(pathsConfig.app.home); - } - - if (accountsResult.error) { - throw accountsResult.error; - } - - return { - account: accountData, - accounts: accountsResult.data, - user, - }; + return workspace.data; }); diff --git a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts index 25c318733..3f89d77e3 100644 --- a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts +++ b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts @@ -9,6 +9,7 @@ import { getBillingGatewayProvider } from '@kit/billing-gateway'; import { getLogger } from '@kit/shared/logger'; import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; +import { createTeamAccountsApi } from '@kit/team-accounts/api'; import appConfig from '~/config/app.config'; import billingConfig from '~/config/billing.config'; @@ -46,11 +47,14 @@ export class TeamBillingService { logger.info(ctx, `Requested checkout session. Processing...`); + const api = createTeamAccountsApi(this.client); + // verify permissions to manage billing - const hasPermission = await getBillingPermissionsForAccountId( + const hasPermission = await api.hasPermission({ userId, accountId, - ); + permission: 'billing.manage', + }); // if the user does not have permission to manage billing for the account // then we should not proceed @@ -73,7 +77,7 @@ export class TeamBillingService { // find the customer ID for the account if it exists // (eg. if the account has been billed before) - const customerId = await getCustomerIdFromAccountId(this.client, accountId); + const customerId = await api.getCustomerId(accountId); const customerEmail = user.email; // the return URL for the checkout session @@ -157,11 +161,14 @@ export class TeamBillingService { const userId = user.id; + const api = createTeamAccountsApi(client); + // we require the user to have permissions to manage billing for the account - const hasPermission = await getBillingPermissionsForAccountId( + const hasPermission = await api.hasPermission({ userId, accountId, - ); + permission: 'billing.manage', + }); // if the user does not have permission to manage billing for the account // then we should not proceed @@ -178,8 +185,7 @@ export class TeamBillingService { throw new Error('Permission denied'); } - const service = await getBillingGatewayProvider(client); - const customerId = await getCustomerIdFromAccountId(client, accountId); + const customerId = await api.getCustomerId(accountId); if (!customerId) { throw new Error('Customer not found'); @@ -195,6 +201,9 @@ export class TeamBillingService { `Creating billing portal session...`, ); + // get the billing gateway provider + const service = await getBillingGatewayProvider(client); + try { const returnUrl = getBillingPortalReturnUrl(slug); @@ -227,7 +236,10 @@ export class TeamBillingService { lineItems: z.infer[], accountId: string, ) { - const variantQuantities = []; + const variantQuantities: Array<{ + quantity: number; + variantId: string; + }> = []; for (const lineItem of lineItems) { if (lineItem.type === 'per-seat') { @@ -246,17 +258,14 @@ export class TeamBillingService { } private async getCurrentMembersCount(accountId: string) { - const { count, error } = await this.client - .from('accounts_memberships') - .select('*', { - head: true, - count: 'exact', - }) - .eq('account_id', accountId); + const api = createTeamAccountsApi(this.client); + const logger = await getLogger(); - if (error) { - const logger = await getLogger(); + try { + const count = await api.getMembersCount(accountId); + return count ?? 1; + } catch (error) { logger.error( { accountId, @@ -266,10 +275,8 @@ export class TeamBillingService { `Encountered an error while fetching the number of existing seats`, ); - throw new Error(); + return Promise.reject(error); } - - return count ?? 1; } } @@ -285,51 +292,6 @@ function getAccountUrl(path: string, slug: string) { return new URL(path, appConfig.url).toString().replace('[account]', slug); } -/** - * @name getBillingPermissionsForAccountId - * @description Retrieves the permissions for a user on an account for managing billing. - */ -async function getBillingPermissionsForAccountId( - userId: string, - accountId: string, -) { - const client = getSupabaseServerActionClient(); - - const { data, error } = await client.rpc('has_permission', { - account_id: accountId, - user_id: userId, - permission_name: 'billing.manage', - }); - - if (error) { - throw error; - } - - return data; -} - -/** - * Retrieves the customer ID based on the provided account ID. - * If it exists we need to pass it to the provider so we can bill the same - * customer ID for the provided account ID - */ -async function getCustomerIdFromAccountId( - client: SupabaseClient, - accountId: string, -) { - const { data, error } = await client - .from('billing_customers') - .select('customer_id') - .eq('account_id', accountId) - .maybeSingle(); - - if (error) { - throw error; - } - - return data?.customer_id; -} - function getPlanDetails(productId: string, planId: string) { const product = billingConfig.products.find( (product) => product.id === productId, diff --git a/apps/web/app/admin/_components/admin-sidebar.tsx b/apps/web/app/admin/_components/admin-sidebar.tsx index a7984006e..69cdc4654 100644 --- a/apps/web/app/admin/_components/admin-sidebar.tsx +++ b/apps/web/app/admin/_components/admin-sidebar.tsx @@ -11,8 +11,8 @@ import { SidebarItem, } from '@kit/ui/sidebar'; -import { ProfileAccountDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown-container'; import { AppLogo } from '~/components/app-logo'; +import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container'; export async function AdminSidebar() { const client = getSupabaseServerActionClient(); diff --git a/apps/web/app/(dashboard)/home/_components/personal-account-dropdown-container.tsx b/apps/web/components/personal-account-dropdown-container.tsx similarity index 100% rename from apps/web/app/(dashboard)/home/_components/personal-account-dropdown-container.tsx rename to apps/web/components/personal-account-dropdown-container.tsx diff --git a/packages/features/accounts/package.json b/packages/features/accounts/package.json index 0ad7072dc..aab422ac8 100644 --- a/packages/features/accounts/package.json +++ b/packages/features/accounts/package.json @@ -12,7 +12,8 @@ "./personal-account-dropdown": "./src/components/personal-account-dropdown.tsx", "./account-selector": "./src/components/account-selector.tsx", "./personal-account-settings": "./src/components/personal-account-settings/index.ts", - "./hooks/*": "./src/hooks/*.ts" + "./hooks/*": "./src/hooks/*.ts", + "./api": "./src/server/api.ts" }, "dependencies": { "@tanstack/react-table": "^8.16.0", diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts new file mode 100644 index 000000000..4d32f6d73 --- /dev/null +++ b/packages/features/accounts/src/server/api.ts @@ -0,0 +1,107 @@ +import { SupabaseClient } from '@supabase/supabase-js'; + +import { Database } from '@kit/supabase/database'; + +/** + * Class representing an API for interacting with user accounts. + * @constructor + * @param {SupabaseClient} client - The Supabase client instance. + */ +class AccountsApi { + constructor(private readonly client: SupabaseClient) {} + + async getAccountWorkspace() { + const { data, error } = await this.client + .from('user_account_workspace') + .select(`*`) + .single(); + + if (error) { + throw error; + } + + return data; + } + + async loadUserAccounts() { + const { data: accounts, error } = await this.client + .from('user_accounts') + .select(`name, slug, picture_url`); + + if (error) { + throw error; + } + + return accounts.map(({ name, slug, picture_url }) => { + return { + label: name, + value: slug, + image: picture_url, + }; + }); + } + + /** + * @name getSubscriptionData + * Get the subscription data for the given user. + * @param accountId + */ + getSubscriptionData(accountId: string) { + return this.client + .from('subscriptions') + .select('*, items: subscription_items !inner (*)') + .eq('account_id', accountId) + .maybeSingle() + .then((response) => { + if (response.error) { + throw response.error; + } + + return response.data; + }); + } + + /** + * Get the orders data for the given account. + * @param accountId + */ + getOrdersData(accountId: string) { + return this.client + .from('orders') + .select('*, items: order_items !inner (*)') + .eq('account_id', accountId) + .maybeSingle() + .then((response) => { + if (response.error) { + throw response.error; + } + + return response.data; + }); + } + + /** + * @name getBillingCustomerId + * Get the billing customer ID for the given user. + * If the user does not have a billing customer ID, it will return null. + * @param accountId + */ + getBillingCustomerId(accountId: string) { + return this.client + .from('billing_customers') + .select('customer_id') + .eq('account_id', accountId) + .maybeSingle() + .then((response) => { + if (response.error) { + throw response.error; + } + + return response.data?.customer_id; + }); + } +} + +export function createAccountsApi(client: SupabaseClient) { + return new AccountsApi(client); +} diff --git a/packages/features/team-accounts/package.json b/packages/features/team-accounts/package.json index e2db4a595..88b9f826f 100644 --- a/packages/features/team-accounts/package.json +++ b/packages/features/team-accounts/package.json @@ -9,6 +9,7 @@ "typecheck": "tsc --noEmit" }, "exports": { + "./api": "./src/server/api.ts", "./components": "./src/components/index.ts", "./webhooks": "./src/server/services/webhooks/index.ts" }, diff --git a/packages/features/team-accounts/src/server/api.ts b/packages/features/team-accounts/src/server/api.ts new file mode 100644 index 000000000..e611d9d69 --- /dev/null +++ b/packages/features/team-accounts/src/server/api.ts @@ -0,0 +1,142 @@ +import { SupabaseClient } from '@supabase/supabase-js'; + +import { Database } from '@kit/supabase/database'; + +/** + * Class representing an API for interacting with team accounts. + * @constructor + * @param {SupabaseClient} client - The Supabase client instance. + */ +export class TeamAccountsApi { + constructor(private readonly client: SupabaseClient) {} + + /** + * @name getAccountWorkspace + * @description Get the account workspace data. + * @param slug + */ + async getAccountWorkspace(slug: string) { + const accountPromise = this.client.rpc('team_account_workspace', { + account_slug: slug, + }); + + const accountsPromise = this.client.from('user_accounts').select('*'); + + const [ + accountResult, + accountsResult, + { + data: { user }, + }, + ] = await Promise.all([ + accountPromise, + accountsPromise, + this.client.auth.getUser(), + ]); + + if (accountResult.error) { + return { + error: accountResult.error, + data: null, + }; + } + + if (accountsResult.error) { + return { + error: accountsResult.error, + data: null, + }; + } + + if (!user) { + return { + error: new Error('User is not logged in'), + data: null, + }; + } + + const accountData = accountResult.data[0]; + + if (!accountData) { + return { + error: new Error('Account data not found'), + data: null, + }; + } + + return { + data: { + account: accountData, + accounts: accountsResult.data, + user, + }, + error: null, + }; + } + + /** + * @name hasPermission + * @description Check if the user has permission to manage billing for the account. + */ + async hasPermission(params: { + accountId: string; + userId: string; + permission: Database['public']['Enums']['app_permissions']; + }) { + const { data, error } = await this.client.rpc('has_permission', { + account_id: params.accountId, + user_id: params.userId, + permission_name: params.permission, + }); + + if (error) { + throw error; + } + + return data; + } + + /** + * @name getMembersCount + * @description Get the number of members in the account. + * @param accountId + */ + async getMembersCount(accountId: string) { + const { count, error } = await this.client + .from('accounts_memberships') + .select('*', { + head: true, + count: 'exact', + }) + .eq('account_id', accountId); + + if (error) { + throw error; + } + + return count; + } + + /** + * @name getCustomerId + * @description Get the billing customer ID for the given account. + * @param accountId + */ + async getCustomerId(accountId: string) { + const { data, error } = await this.client + .from('billing_customers') + .select('customer_id') + .eq('account_id', accountId) + .maybeSingle(); + + if (error) { + throw error; + } + + return data?.customer_id; + } +} + +export function createTeamAccountsApi(client: SupabaseClient) { + return new TeamAccountsApi(client); +} diff --git a/packages/supabase/package.json b/packages/supabase/package.json index 76d44df78..59bd8b7bf 100644 --- a/packages/supabase/package.json +++ b/packages/supabase/package.json @@ -10,7 +10,6 @@ }, "prettier": "@kit/prettier-config", "exports": { - ".": "./src/index.ts", "./middleware-client": "./src/clients/middleware.client.ts", "./server-actions-client": "./src/clients/server-actions.client.ts", "./route-handler-client": "./src/clients/route-handler.client.ts",