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 { 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();
|
||||
@@ -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<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 { 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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<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 { 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;
|
||||
});
|
||||
|
||||
@@ -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<typeof LineItemSchema>[],
|
||||
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<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) {
|
||||
const product = billingConfig.products.find(
|
||||
(product) => product.id === productId,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
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"
|
||||
},
|
||||
"exports": {
|
||||
"./api": "./src/server/api.ts",
|
||||
"./components": "./src/components/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",
|
||||
"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",
|
||||
|
||||
Reference in New Issue
Block a user