Refactor billing services with new AccountsApi
The billing services have been refactored to use the new AccountsApi and TeamAccountsApi. All methods that were previously defined in each billing service, including getting customer ID, getting permissions, etc., have been transferred to these APIs. This change improves the modularity and organization of the code.
This commit is contained in:
@@ -5,14 +5,14 @@ import { cookies } from 'next/headers';
|
|||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar';
|
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 { AppLogo } from '~/components/app-logo';
|
||||||
|
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
|
||||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||||
import { personalAccountSidebarConfig } from '~/config/personal-account-sidebar.config';
|
import { personalAccountSidebarConfig } from '~/config/personal-account-sidebar.config';
|
||||||
|
|
||||||
// home imports
|
// home imports
|
||||||
import { HomeSidebarAccountSelector } from '../_components/home-sidebar-account-selector';
|
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() {
|
export function HomeSidebar() {
|
||||||
const collapsed = getSidebarCollapsed();
|
const collapsed = getSidebarCollapsed();
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { cache } from 'react';
|
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 { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
|
|
||||||
import featureFlagsConfig from '~/config/feature-flags.config';
|
import featureFlagsConfig from '~/config/feature-flags.config';
|
||||||
import { Database } from '~/lib/database.types';
|
|
||||||
|
|
||||||
const shouldLoadAccounts = featureFlagsConfig.enableTeamAccounts;
|
const shouldLoadAccounts = featureFlagsConfig.enableTeamAccounts;
|
||||||
|
|
||||||
@@ -17,12 +15,13 @@ const shouldLoadAccounts = featureFlagsConfig.enableTeamAccounts;
|
|||||||
*/
|
*/
|
||||||
export const loadUserWorkspace = cache(async () => {
|
export const loadUserWorkspace = cache(async () => {
|
||||||
const client = getSupabaseServerComponentClient();
|
const client = getSupabaseServerComponentClient();
|
||||||
|
const api = createAccountsApi(client);
|
||||||
|
|
||||||
const accountsPromise = shouldLoadAccounts
|
const accountsPromise = shouldLoadAccounts
|
||||||
? () => loadUserAccounts(client)
|
? () => api.loadUserAccounts()
|
||||||
: () => Promise.resolve([]);
|
: () => Promise.resolve([]);
|
||||||
|
|
||||||
const workspacePromise = loadUserAccountWorkspace(client);
|
const workspacePromise = api.getAccountWorkspace();
|
||||||
const userPromise = client.auth.getUser();
|
const userPromise = client.auth.getUser();
|
||||||
|
|
||||||
const [accounts, workspace, userResult] = await Promise.all([
|
const [accounts, workspace, userResult] = await Promise.all([
|
||||||
@@ -43,34 +42,3 @@ export const loadUserWorkspace = cache(async () => {
|
|||||||
user,
|
user,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadUserAccountWorkspace(client: SupabaseClient<Database>) {
|
|
||||||
const { data, error } = await client
|
|
||||||
.from('user_account_workspace')
|
|
||||||
.select(`*`)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUserAccounts(client: SupabaseClient<Database>) {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -6,10 +6,9 @@ import { SupabaseClient } from '@supabase/supabase-js';
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { createAccountsApi } from '@kit/accounts/api';
|
||||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
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
|
* 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
|
* 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) => {
|
export const loadPersonalAccountBillingPageData = cache((userId: string) => {
|
||||||
const client = getSupabaseServerComponentClient();
|
const client = getSupabaseServerComponentClient();
|
||||||
|
const api = createAccountsApi(client);
|
||||||
|
|
||||||
const data =
|
const data =
|
||||||
BILLING_MODE === 'subscription'
|
BILLING_MODE === 'subscription'
|
||||||
? getSubscriptionData(client, userId)
|
? api.getSubscriptionData(userId)
|
||||||
: getOrdersData(client, userId);
|
: api.getOrdersData(userId);
|
||||||
|
|
||||||
const customerId = getBillingCustomerId(client, userId);
|
const customerId = api.getBillingCustomerId(userId);
|
||||||
|
|
||||||
return Promise.all([data, customerId]);
|
return Promise.all([data, customerId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the subscription data for the given user.
|
|
||||||
* @param client
|
|
||||||
* @param userId
|
|
||||||
*/
|
|
||||||
function getSubscriptionData(client: SupabaseClient<Database>, 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<Database>, 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<Database>,
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { SupabaseClient } from '@supabase/supabase-js';
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { createAccountsApi } from '@kit/accounts/api';
|
||||||
import { getProductPlanPair } from '@kit/billing';
|
import { getProductPlanPair } from '@kit/billing';
|
||||||
import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
import { requireUser } from '@kit/supabase/require-user';
|
import { requireUser } from '@kit/supabase/require-user';
|
||||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
|
||||||
|
|
||||||
import appConfig from '~/config/app.config';
|
import appConfig from '~/config/app.config';
|
||||||
import billingConfig from '~/config/billing.config';
|
import billingConfig from '~/config/billing.config';
|
||||||
@@ -22,6 +22,12 @@ export class UserBillingService {
|
|||||||
|
|
||||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name createCheckoutSession
|
||||||
|
* @description Create a checkout session for the user
|
||||||
|
* @param planId
|
||||||
|
* @param productId
|
||||||
|
*/
|
||||||
async createCheckoutSession({
|
async createCheckoutSession({
|
||||||
planId,
|
planId,
|
||||||
productId,
|
productId,
|
||||||
@@ -44,7 +50,8 @@ export class UserBillingService {
|
|||||||
|
|
||||||
// find the customer ID for the account if it exists
|
// find the customer ID for the account if it exists
|
||||||
// (eg. if the account has been billed before)
|
// (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(
|
const product = billingConfig.products.find(
|
||||||
(item) => item.id === productId,
|
(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() {
|
async createBillingPortalSession() {
|
||||||
const { data, error } = await requireUser(this.client);
|
const { data, error } = await requireUser(this.client);
|
||||||
|
|
||||||
@@ -118,7 +130,8 @@ export class UserBillingService {
|
|||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
const accountId = data.id;
|
const accountId = data.id;
|
||||||
const customerId = await getCustomerIdFromAccountId(accountId);
|
const api = createAccountsApi(this.client);
|
||||||
|
const customerId = await api.getBillingCustomerId(accountId);
|
||||||
const returnUrl = getBillingPortalReturnUrl();
|
const returnUrl = getBillingPortalReturnUrl();
|
||||||
|
|
||||||
if (!customerId) {
|
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() {
|
function getCheckoutSessionReturnUrl() {
|
||||||
return new URL(
|
return new URL(
|
||||||
pathsConfig.app.personalAccountBillingReturn,
|
pathsConfig.app.personalAccountBillingReturn,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Page } from '@kit/ui/page';
|
import { Page } from '@kit/ui/page';
|
||||||
|
|
||||||
import { HomeSidebar } from '~/(dashboard)/home/_components/home-sidebar';
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
import { HomeSidebar } from './_components/home-sidebar';
|
||||||
|
|
||||||
function UserHomeLayout({ children }: React.PropsWithChildren) {
|
function UserHomeLayout({ children }: React.PropsWithChildren) {
|
||||||
return <Page sidebar={<HomeSidebar />}>{children}</Page>;
|
return <Page sidebar={<HomeSidebar />}>{children}</Page>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { cn } from '@kit/ui/utils';
|
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 featureFlagsConfig from '~/config/feature-flags.config';
|
||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ import 'server-only';
|
|||||||
|
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
|
|
||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { createAccountsApi } from '@kit/accounts/api';
|
||||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
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
|
* 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
|
* 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) => {
|
export const loadTeamAccountBillingPage = cache((accountId: string) => {
|
||||||
const client = getSupabaseServerComponentClient();
|
const client = getSupabaseServerComponentClient();
|
||||||
|
const api = createAccountsApi(client);
|
||||||
|
|
||||||
const data =
|
const data =
|
||||||
BILLING_MODE === 'subscription'
|
BILLING_MODE === 'subscription'
|
||||||
? getSubscriptionData(client, accountId)
|
? api.getSubscriptionData(accountId)
|
||||||
: getOrdersData(client, accountId);
|
: api.getOrdersData(accountId);
|
||||||
|
|
||||||
const customerId = getBillingCustomerId(client, accountId);
|
const customerId = api.getBillingCustomerId(accountId);
|
||||||
|
|
||||||
return Promise.all([data, customerId]);
|
return Promise.all([data, customerId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the subscription data for the given user.
|
|
||||||
* @param client
|
|
||||||
* @param accountId
|
|
||||||
*/
|
|
||||||
function getSubscriptionData(
|
|
||||||
client: SupabaseClient<Database>,
|
|
||||||
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<Database>, 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<Database>,
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { cache } from 'react';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
|
import { createTeamAccountsApi } from '@kit/team-accounts/api';
|
||||||
|
|
||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
|
||||||
@@ -19,50 +20,21 @@ import pathsConfig from '~/config/paths.config';
|
|||||||
*/
|
*/
|
||||||
export const loadTeamWorkspace = cache(async (accountSlug: string) => {
|
export const loadTeamWorkspace = cache(async (accountSlug: string) => {
|
||||||
const client = getSupabaseServerComponentClient();
|
const client = getSupabaseServerComponentClient();
|
||||||
|
const api = createTeamAccountsApi(client);
|
||||||
|
|
||||||
const accountPromise = client.rpc('team_account_workspace', {
|
const workspace = await api.getAccountWorkspace(accountSlug);
|
||||||
account_slug: accountSlug,
|
|
||||||
});
|
|
||||||
|
|
||||||
const accountsPromise = client.from('user_accounts').select('*');
|
if (workspace.error) {
|
||||||
|
throw workspace.error;
|
||||||
const [
|
|
||||||
accountResult,
|
|
||||||
accountsResult,
|
|
||||||
{
|
|
||||||
data: { user },
|
|
||||||
},
|
|
||||||
] = await Promise.all([
|
|
||||||
accountPromise,
|
|
||||||
accountsPromise,
|
|
||||||
client.auth.getUser(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (accountResult.error) {
|
|
||||||
throw accountResult.error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const account = workspace.data.account;
|
||||||
|
|
||||||
// we cannot find any record for the selected account
|
// we cannot find any record for the selected account
|
||||||
// so we redirect the user to the home page
|
// so we redirect the user to the home page
|
||||||
if (!accountResult.data.length) {
|
if (!account) {
|
||||||
return redirect(pathsConfig.app.home);
|
return redirect(pathsConfig.app.home);
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountData = accountResult.data[0];
|
return workspace.data;
|
||||||
|
|
||||||
// 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,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
|||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
import { requireUser } from '@kit/supabase/require-user';
|
import { requireUser } from '@kit/supabase/require-user';
|
||||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||||
|
import { createTeamAccountsApi } from '@kit/team-accounts/api';
|
||||||
|
|
||||||
import appConfig from '~/config/app.config';
|
import appConfig from '~/config/app.config';
|
||||||
import billingConfig from '~/config/billing.config';
|
import billingConfig from '~/config/billing.config';
|
||||||
@@ -46,11 +47,14 @@ export class TeamBillingService {
|
|||||||
|
|
||||||
logger.info(ctx, `Requested checkout session. Processing...`);
|
logger.info(ctx, `Requested checkout session. Processing...`);
|
||||||
|
|
||||||
|
const api = createTeamAccountsApi(this.client);
|
||||||
|
|
||||||
// verify permissions to manage billing
|
// verify permissions to manage billing
|
||||||
const hasPermission = await getBillingPermissionsForAccountId(
|
const hasPermission = await api.hasPermission({
|
||||||
userId,
|
userId,
|
||||||
accountId,
|
accountId,
|
||||||
);
|
permission: 'billing.manage',
|
||||||
|
});
|
||||||
|
|
||||||
// if the user does not have permission to manage billing for the account
|
// if the user does not have permission to manage billing for the account
|
||||||
// then we should not proceed
|
// then we should not proceed
|
||||||
@@ -73,7 +77,7 @@ export class TeamBillingService {
|
|||||||
|
|
||||||
// find the customer ID for the account if it exists
|
// find the customer ID for the account if it exists
|
||||||
// (eg. if the account has been billed before)
|
// (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;
|
const customerEmail = user.email;
|
||||||
|
|
||||||
// the return URL for the checkout session
|
// the return URL for the checkout session
|
||||||
@@ -157,11 +161,14 @@ export class TeamBillingService {
|
|||||||
|
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
|
|
||||||
|
const api = createTeamAccountsApi(client);
|
||||||
|
|
||||||
// we require the user to have permissions to manage billing for the account
|
// we require the user to have permissions to manage billing for the account
|
||||||
const hasPermission = await getBillingPermissionsForAccountId(
|
const hasPermission = await api.hasPermission({
|
||||||
userId,
|
userId,
|
||||||
accountId,
|
accountId,
|
||||||
);
|
permission: 'billing.manage',
|
||||||
|
});
|
||||||
|
|
||||||
// if the user does not have permission to manage billing for the account
|
// if the user does not have permission to manage billing for the account
|
||||||
// then we should not proceed
|
// then we should not proceed
|
||||||
@@ -178,8 +185,7 @@ export class TeamBillingService {
|
|||||||
throw new Error('Permission denied');
|
throw new Error('Permission denied');
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = await getBillingGatewayProvider(client);
|
const customerId = await api.getCustomerId(accountId);
|
||||||
const customerId = await getCustomerIdFromAccountId(client, accountId);
|
|
||||||
|
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
throw new Error('Customer not found');
|
throw new Error('Customer not found');
|
||||||
@@ -195,6 +201,9 @@ export class TeamBillingService {
|
|||||||
`Creating billing portal session...`,
|
`Creating billing portal session...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// get the billing gateway provider
|
||||||
|
const service = await getBillingGatewayProvider(client);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const returnUrl = getBillingPortalReturnUrl(slug);
|
const returnUrl = getBillingPortalReturnUrl(slug);
|
||||||
|
|
||||||
@@ -227,7 +236,10 @@ export class TeamBillingService {
|
|||||||
lineItems: z.infer<typeof LineItemSchema>[],
|
lineItems: z.infer<typeof LineItemSchema>[],
|
||||||
accountId: string,
|
accountId: string,
|
||||||
) {
|
) {
|
||||||
const variantQuantities = [];
|
const variantQuantities: Array<{
|
||||||
|
quantity: number;
|
||||||
|
variantId: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
for (const lineItem of lineItems) {
|
for (const lineItem of lineItems) {
|
||||||
if (lineItem.type === 'per-seat') {
|
if (lineItem.type === 'per-seat') {
|
||||||
@@ -246,17 +258,14 @@ export class TeamBillingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getCurrentMembersCount(accountId: string) {
|
private async getCurrentMembersCount(accountId: string) {
|
||||||
const { count, error } = await this.client
|
const api = createTeamAccountsApi(this.client);
|
||||||
.from('accounts_memberships')
|
const logger = await getLogger();
|
||||||
.select('*', {
|
|
||||||
head: true,
|
|
||||||
count: 'exact',
|
|
||||||
})
|
|
||||||
.eq('account_id', accountId);
|
|
||||||
|
|
||||||
if (error) {
|
try {
|
||||||
const logger = await getLogger();
|
const count = await api.getMembersCount(accountId);
|
||||||
|
|
||||||
|
return count ?? 1;
|
||||||
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{
|
{
|
||||||
accountId,
|
accountId,
|
||||||
@@ -266,10 +275,8 @@ export class TeamBillingService {
|
|||||||
`Encountered an error while fetching the number of existing seats`,
|
`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);
|
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<Database>,
|
|
||||||
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) {
|
function getPlanDetails(productId: string, planId: string) {
|
||||||
const product = billingConfig.products.find(
|
const product = billingConfig.products.find(
|
||||||
(product) => product.id === productId,
|
(product) => product.id === productId,
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
SidebarItem,
|
SidebarItem,
|
||||||
} from '@kit/ui/sidebar';
|
} from '@kit/ui/sidebar';
|
||||||
|
|
||||||
import { ProfileAccountDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown-container';
|
|
||||||
import { AppLogo } from '~/components/app-logo';
|
import { AppLogo } from '~/components/app-logo';
|
||||||
|
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
|
||||||
|
|
||||||
export async function AdminSidebar() {
|
export async function AdminSidebar() {
|
||||||
const client = getSupabaseServerActionClient();
|
const client = getSupabaseServerActionClient();
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"./personal-account-dropdown": "./src/components/personal-account-dropdown.tsx",
|
"./personal-account-dropdown": "./src/components/personal-account-dropdown.tsx",
|
||||||
"./account-selector": "./src/components/account-selector.tsx",
|
"./account-selector": "./src/components/account-selector.tsx",
|
||||||
"./personal-account-settings": "./src/components/personal-account-settings/index.ts",
|
"./personal-account-settings": "./src/components/personal-account-settings/index.ts",
|
||||||
"./hooks/*": "./src/hooks/*.ts"
|
"./hooks/*": "./src/hooks/*.ts",
|
||||||
|
"./api": "./src/server/api.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-table": "^8.16.0",
|
"@tanstack/react-table": "^8.16.0",
|
||||||
|
|||||||
107
packages/features/accounts/src/server/api.ts
Normal file
107
packages/features/accounts/src/server/api.ts
Normal file
@@ -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<Database>} client - The Supabase client instance.
|
||||||
|
*/
|
||||||
|
class AccountsApi {
|
||||||
|
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
|
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<Database>) {
|
||||||
|
return new AccountsApi(client);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
"./api": "./src/server/api.ts",
|
||||||
"./components": "./src/components/index.ts",
|
"./components": "./src/components/index.ts",
|
||||||
"./webhooks": "./src/server/services/webhooks/index.ts"
|
"./webhooks": "./src/server/services/webhooks/index.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
142
packages/features/team-accounts/src/server/api.ts
Normal file
142
packages/features/team-accounts/src/server/api.ts
Normal file
@@ -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<Database>} client - The Supabase client instance.
|
||||||
|
*/
|
||||||
|
export class TeamAccountsApi {
|
||||||
|
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<Database>) {
|
||||||
|
return new TeamAccountsApi(client);
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@kit/prettier-config",
|
"prettier": "@kit/prettier-config",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
|
||||||
"./middleware-client": "./src/clients/middleware.client.ts",
|
"./middleware-client": "./src/clients/middleware.client.ts",
|
||||||
"./server-actions-client": "./src/clients/server-actions.client.ts",
|
"./server-actions-client": "./src/clients/server-actions.client.ts",
|
||||||
"./route-handler-client": "./src/clients/route-handler.client.ts",
|
"./route-handler-client": "./src/clients/route-handler.client.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user