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:
giancarlo
2024-04-22 01:35:15 +08:00
parent 75c438a5f2
commit 866b9888f7
17 changed files with 324 additions and 323 deletions

View File

@@ -0,0 +1,38 @@
'use client';
import { useRouter } from 'next/navigation';
import { AccountSelector } from '@kit/accounts/account-selector';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
const features = {
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
};
export function HomeSidebarAccountSelector(props: {
accounts: Array<{
label: string | null;
value: string | null;
image: string | null;
}>;
collapsed: boolean;
}) {
const router = useRouter();
return (
<AccountSelector
collapsed={props.collapsed}
accounts={props.accounts}
features={features}
onAccountChange={(value) => {
if (value) {
const path = pathsConfig.app.accountHome.replace('[account]', value);
router.replace(path);
}
}}
/>
);
}

View File

@@ -0,0 +1,54 @@
import { use } from 'react';
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';
export function HomeSidebar() {
const collapsed = getSidebarCollapsed();
const { accounts, user, workspace } = use(loadUserWorkspace());
return (
<Sidebar collapsed={collapsed}>
<SidebarContent className={'h-16 justify-center'}>
<If
condition={featuresFlagConfig.enableTeamAccounts}
fallback={<AppLogo className={'py-2'} />}
>
<HomeSidebarAccountSelector
collapsed={collapsed}
accounts={accounts}
/>
</If>
</SidebarContent>
<SidebarContent className={`mt-5 h-[calc(100%-160px)] overflow-y-auto`}>
<SidebarNavigation config={personalAccountSidebarConfig} />
</SidebarContent>
<div className={'absolute bottom-4 left-0 w-full'}>
<SidebarContent>
<ProfileAccountDropdownContainer
collapsed={collapsed}
user={user}
account={workspace}
/>
</SidebarContent>
</div>
</Sidebar>
);
}
function getSidebarCollapsed() {
return cookies().get('sidebar-collapsed')?.value === 'true';
}

View File

@@ -0,0 +1,44 @@
import { cache } from 'react';
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import featureFlagsConfig from '~/config/feature-flags.config';
const shouldLoadAccounts = featureFlagsConfig.enableTeamAccounts;
/**
* @name loadUserWorkspace
* @description
* Load the user workspace data. It's a cached per-request function that fetches the user workspace data.
* It can be used across the server components to load the user workspace data.
*/
export const loadUserWorkspace = cache(async () => {
const client = getSupabaseServerComponentClient();
const api = createAccountsApi(client);
const accountsPromise = shouldLoadAccounts
? () => api.loadUserAccounts()
: () => Promise.resolve([]);
const workspacePromise = api.getAccountWorkspace();
const userPromise = client.auth.getUser();
const [accounts, workspace, userResult] = await Promise.all([
accountsPromise(),
workspacePromise,
userPromise,
]);
const user = userResult.data.user;
if (!user) {
throw new Error('User is not logged in');
}
return {
accounts,
workspace,
user,
};
});

View File

@@ -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<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;
});
}

View File

@@ -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<Database>) {}
/**
* @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,

View File

@@ -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 <Page sidebar={<HomeSidebar />}>{children}</Page>;
}