Refactor billing data loading and schema configuration

This update enhances the capability of the billing system to handle different types of billing modes (subscription-based or one-time payments) and introduces an environment variable for configuring the preferred billing mode. It also refactors the data fetching process in the `loadPersonalAccountBillingPageData` and `loadTeamAccountBillingPageData` to retrieve the proper data depending on the chosen billing mode. Detailed documentation was added in README.md to guide the configuration of the billing schema.
This commit is contained in:
giancarlo
2024-04-22 00:35:03 +08:00
parent b393d94fb2
commit ecb20b8917
6 changed files with 596 additions and 26 deletions

View File

@@ -2,24 +2,108 @@ import 'server-only';
import { cache } from 'react';
import { SupabaseClient } from '@supabase/supabase-js';
import { z } from 'zod';
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
* value is 'subscription'. The value can be overridden by the environment variable
* BILLING_MODE.
*
* If the value is 'subscription', we fetch the subscription data for the user.
* If the value is 'one-time', we fetch the orders data for the user.
* if none of these suits your needs, please override the below function.
*/
const BILLING_MODE = z
.enum(['subscription', 'one-time'])
.default('subscription')
.parse(process.env.BILLING_MODE);
/**
* Load the personal account billing page data for the given user.
* @param userId
* @returns The subscription data or the orders data and the billing customer ID.
* This function is cached per-request.
*/
export const loadPersonalAccountBillingPageData = cache((userId: string) => {
const client = getSupabaseServerComponentClient();
const subscription = client
const data =
BILLING_MODE === 'subscription'
? getSubscriptionData(client, userId)
: getOrdersData(client, userId);
const customerId = getBillingCustomerId(client, 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(({ data }) => data);
.then((response) => {
if (response.error) {
throw response.error;
}
const customer = client
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(({ data }) => data?.customer_id);
.then((response) => {
if (response.error) {
throw response.error;
}
return Promise.all([subscription, customer]);
});
return response.data?.customer_id;
});
}

View File

@@ -10,12 +10,13 @@ import { getLogger } from '@kit/shared/logger';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { PersonalAccountCheckoutSchema } from '~/(dashboard)/home/(user)/billing/_lib/schema/personal-account-checkout.schema';
import appConfig from '~/config/app.config';
import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config';
import { Database } from '~/lib/database.types';
import { PersonalAccountCheckoutSchema } from '../schema/personal-account-checkout.schema';
export class UserBillingService {
private readonly namespace = 'billing.personal-account';

View File

@@ -2,6 +2,7 @@ import { redirect } from 'next/navigation';
import {
BillingPortalCard,
CurrentLifetimeOrderCard,
CurrentSubscriptionCard,
} from '@kit/billing-gateway/components';
import { requireUser } from '@kit/supabase/require-user';
@@ -37,7 +38,7 @@ async function PersonalAccountBillingPage() {
redirect(auth.redirectTo);
}
const [subscription, customerId] = await loadPersonalAccountBillingPageData(
const [data, customerId] = await loadPersonalAccountBillingPageData(
auth.data.id,
);
@@ -50,7 +51,7 @@ async function PersonalAccountBillingPage() {
<PageBody>
<div className={'flex flex-col space-y-8'}>
<If condition={!subscription}>
<If condition={!data}>
<PersonalAccountCheckoutForm customerId={customerId} />
<If condition={customerId}>
@@ -58,15 +59,26 @@ async function PersonalAccountBillingPage() {
</If>
</If>
<If condition={subscription}>
<If condition={data}>
{(subscription) => (
<div
className={'mx-auto flex w-full max-w-2xl flex-col space-y-6'}
>
<CurrentSubscriptionCard
subscription={subscription}
config={billingConfig}
/>
<If condition={data}>
<CurrentSubscriptionCard
subscription={data.}
config={billingConfig}
/>
</If>
<If condition={!data}>
<PersonalAccountCheckoutForm customerId={customerId} />
<CurrentLifetimeOrderCard
order={data}
config={billingConfig}
/>
</If>
<If condition={customerId}>
<CustomerBillingPortalForm />

View File

@@ -2,25 +2,105 @@ import 'server-only';
import { cache } from 'react';
import { SupabaseClient } from '@supabase/supabase-js';
import { z } from 'zod';
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
* value is 'subscription'. The value can be overridden by the environment variable
* BILLING_MODE.
*
* If the value is 'subscription', we fetch the subscription data for the user.
* If the value is 'one-time', we fetch the orders data for the user.
* if none of these suits your needs, please override the below function.
*/
const BILLING_MODE = z
.enum(['subscription', 'one-time'])
.default('subscription')
.parse(process.env.BILLING_MODE);
export const loadTeamAccountBillingPage = cache((accountId: string) => {
const client = getSupabaseServerComponentClient();
// TODO: improve these queries to only load the necessary data
const subscription = client
const data =
BILLING_MODE === 'subscription'
? getSubscriptionData(client, accountId)
: getOrdersData(client, accountId);
const customerId = getBillingCustomerId(client, 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(({ data }) => data);
.then((response) => {
if (response.error) {
throw response.error;
}
const customerId = client
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(({ data }) => data?.customer_id);
.then((response) => {
if (response.error) {
throw response.error;
}
return Promise.all([subscription, customerId]);
});
return response.data?.customer_id;
});
}

View File

@@ -7,6 +7,8 @@ import { getSupabaseServerComponentClient } from '@kit/supabase/server-component
import featureFlagsConfig from '~/config/feature-flags.config';
import { Database } from '~/lib/database.types';
const shouldLoadAccounts = featureFlagsConfig.enableTeamAccounts;
/**
* @name loadUserWorkspace
* @description
@@ -15,17 +17,16 @@ import { Database } from '~/lib/database.types';
*/
export const loadUserWorkspace = cache(async () => {
const client = getSupabaseServerComponentClient();
const loadAccounts = featureFlagsConfig.enableTeamAccounts;
const accountsPromise = loadAccounts
? loadUserAccounts(client)
: Promise.resolve([]);
const accountsPromise = shouldLoadAccounts
? () => loadUserAccounts(client)
: () => Promise.resolve([]);
const workspacePromise = loadUserAccountWorkspace(client);
const userPromise = client.auth.getUser();
const [accounts, workspace, userResult] = await Promise.all([
accountsPromise,
accountsPromise(),
workspacePromise,
userPromise,
]);