Refactor and improve billing module
The billing module has been refined and enhanced to include deeper validation and detailing of billing plans and products. The checkout session creation process was revised to handle more complex scenarios, incorporating better parsing and validation. Additional validations were added for the plan and product schemas, improving product details extraction, and rearranging of module exports was made for better organization. The code refactor allows easier future modifications and upgrades for recurring and one-time payments with nuanced product configurations.
This commit is contained in:
@@ -54,12 +54,13 @@ export function PersonalAccountCheckoutForm() {
|
||||
<PlanPicker
|
||||
pending={pending}
|
||||
config={billingConfig}
|
||||
onSubmit={({ planId }) => {
|
||||
onSubmit={({ planId, productId }) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const { checkoutToken } =
|
||||
await createPersonalAccountCheckoutSession({
|
||||
planId,
|
||||
productId,
|
||||
});
|
||||
|
||||
setCheckoutToken(checkoutToken);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getProductPlanPairFromId } from '@kit/billing';
|
||||
import { getLineItemsFromPlanId } from '@kit/billing';
|
||||
import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { requireAuth } from '@kit/supabase/require-auth';
|
||||
@@ -22,6 +22,7 @@ import pathsConfig from '~/config/paths.config';
|
||||
*/
|
||||
export async function createPersonalAccountCheckoutSession(params: {
|
||||
planId: string;
|
||||
productId: string;
|
||||
}) {
|
||||
const client = getSupabaseServerActionClient();
|
||||
const { data, error } = await requireAuth(client);
|
||||
@@ -30,21 +31,22 @@ export async function createPersonalAccountCheckoutSession(params: {
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
|
||||
const planId = z.string().min(1).parse(params.planId);
|
||||
const { planId, productId } = z
|
||||
.object({
|
||||
planId: z.string().min(1),
|
||||
productId: z.string().min(1),
|
||||
})
|
||||
.parse(params);
|
||||
|
||||
Logger.info(
|
||||
{
|
||||
planId,
|
||||
productId,
|
||||
},
|
||||
`Creating checkout session for plan ID`,
|
||||
);
|
||||
|
||||
const service = await getBillingGatewayProvider(client);
|
||||
const productPlanPairFromId = getProductPlanPairFromId(billingConfig, planId);
|
||||
|
||||
if (!productPlanPairFromId) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
|
||||
// in the case of personal accounts
|
||||
// the account ID is the same as the user ID
|
||||
@@ -57,16 +59,21 @@ export async function createPersonalAccountCheckoutSession(params: {
|
||||
// (eg. if the account has been billed before)
|
||||
const customerId = await getCustomerIdFromAccountId(accountId);
|
||||
|
||||
// retrieve the product and plan from the billing configuration
|
||||
const { product, plan } = productPlanPairFromId;
|
||||
const product = billingConfig.products.find((item) => item.id === productId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
|
||||
const { lineItems, trialDays } = getLineItemsFromPlanId(product, planId);
|
||||
|
||||
// call the payment gateway to create the checkout session
|
||||
const { checkoutToken } = await service.createCheckoutSession({
|
||||
paymentType: product.paymentType,
|
||||
lineItems,
|
||||
returnUrl,
|
||||
accountId,
|
||||
planId,
|
||||
trialPeriodDays: plan.trialPeriodDays,
|
||||
trialDays,
|
||||
paymentType: product.paymentType,
|
||||
customerEmail: data.user.email,
|
||||
customerId,
|
||||
});
|
||||
|
||||
@@ -45,14 +45,15 @@ export function TeamAccountCheckoutForm(params: { accountId: string }) {
|
||||
<PlanPicker
|
||||
pending={pending}
|
||||
config={billingConfig}
|
||||
onSubmit={({ planId }) => {
|
||||
onSubmit={({ planId, productId }) => {
|
||||
startTransition(async () => {
|
||||
const slug = routeParams.account as string;
|
||||
|
||||
const { checkoutToken } = await createTeamAccountCheckoutSession({
|
||||
planId,
|
||||
accountId: params.accountId,
|
||||
productId,
|
||||
slug,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
|
||||
setCheckoutToken(checkoutToken);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getProductPlanPairFromId } from '@kit/billing';
|
||||
import { getLineItemsFromPlanId } from '@kit/billing';
|
||||
import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
||||
import { requireAuth } from '@kit/supabase/require-auth';
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
@@ -20,6 +20,7 @@ import pathsConfig from '~/config/paths.config';
|
||||
* @param {string} params.planId - The ID of the plan to be associated with the account.
|
||||
*/
|
||||
export async function createTeamAccountCheckoutSession(params: {
|
||||
productId: string;
|
||||
planId: string;
|
||||
accountId: string;
|
||||
slug: string;
|
||||
@@ -29,6 +30,7 @@ export async function createTeamAccountCheckoutSession(params: {
|
||||
// we parse the plan ID from the parameters
|
||||
// no need in continuing if the plan ID is not valid
|
||||
const planId = z.string().min(1).parse(params.planId);
|
||||
const productId = z.string().min(1).parse(params.productId);
|
||||
|
||||
// we require the user to be authenticated
|
||||
const { data: session } = await requireAuth(client);
|
||||
@@ -51,32 +53,34 @@ export async function createTeamAccountCheckoutSession(params: {
|
||||
// here we have confirmed that the user has permission to manage billing for the account
|
||||
// so we go on and create a checkout session
|
||||
const service = await getBillingGatewayProvider(client);
|
||||
const productPlanPairFromId = getProductPlanPairFromId(billingConfig, planId);
|
||||
|
||||
if (!productPlanPairFromId) {
|
||||
const product = billingConfig.products.find(
|
||||
(product) => product.id === productId,
|
||||
);
|
||||
|
||||
if (!product) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
|
||||
// the return URL for the checkout session
|
||||
const returnUrl = getCheckoutSessionReturnUrl(params.slug);
|
||||
const { lineItems, trialDays } = getLineItemsFromPlanId(product, planId);
|
||||
|
||||
// find the customer ID for the account if it exists
|
||||
// (eg. if the account has been billed before)
|
||||
const customerId = await getCustomerIdFromAccountId(client, accountId);
|
||||
const customerEmail = session.user.email;
|
||||
|
||||
// retrieve the product and plan from the billing configuration
|
||||
const { product, plan } = productPlanPairFromId;
|
||||
// the return URL for the checkout session
|
||||
const returnUrl = getCheckoutSessionReturnUrl(params.slug);
|
||||
|
||||
// call the payment gateway to create the checkout session
|
||||
const { checkoutToken } = await service.createCheckoutSession({
|
||||
accountId,
|
||||
lineItems,
|
||||
returnUrl,
|
||||
planId,
|
||||
customerEmail,
|
||||
customerId,
|
||||
trialDays,
|
||||
paymentType: product.paymentType,
|
||||
trialPeriodDays: plan.trialPeriodDays,
|
||||
});
|
||||
|
||||
// return the checkout token to the client
|
||||
|
||||
@@ -8,22 +8,24 @@ export default createBillingSchema({
|
||||
name: 'Starter',
|
||||
description: 'The perfect plan to get started',
|
||||
currency: 'USD',
|
||||
paymentType: 'recurring',
|
||||
badge: `Value`,
|
||||
paymentType: 'recurring',
|
||||
plans: [
|
||||
{
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
|
||||
name: 'Starter Monthly',
|
||||
price: '9.99',
|
||||
interval: 'month',
|
||||
perSeat: false,
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
|
||||
price: 9.99,
|
||||
recurring: {
|
||||
interval: 'month',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'starter-yearly',
|
||||
name: 'Starter Yearly',
|
||||
price: '99.99',
|
||||
interval: 'year',
|
||||
perSeat: false,
|
||||
id: 'starter-yearly',
|
||||
price: 99.99,
|
||||
recurring: {
|
||||
interval: 'year',
|
||||
},
|
||||
},
|
||||
],
|
||||
features: ['Feature 1', 'Feature 2', 'Feature 3'],
|
||||
@@ -34,22 +36,24 @@ export default createBillingSchema({
|
||||
badge: `Popular`,
|
||||
highlighted: true,
|
||||
description: 'The perfect plan for professionals',
|
||||
paymentType: 'recurring',
|
||||
currency: 'USD',
|
||||
paymentType: 'recurring',
|
||||
plans: [
|
||||
{
|
||||
id: 'pro-monthly',
|
||||
name: 'Pro Monthly',
|
||||
price: '19.99',
|
||||
interval: 'month',
|
||||
perSeat: false,
|
||||
id: 'pro-monthly',
|
||||
price: 19.99,
|
||||
recurring: {
|
||||
interval: 'month',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pro-yearly',
|
||||
name: 'Pro Yearly',
|
||||
price: '199.99',
|
||||
interval: 'year',
|
||||
perSeat: false,
|
||||
id: 'pro-yearly',
|
||||
price: 199.99,
|
||||
recurring: {
|
||||
interval: 'year',
|
||||
},
|
||||
},
|
||||
],
|
||||
features: [
|
||||
@@ -64,22 +68,24 @@ export default createBillingSchema({
|
||||
id: 'enterprise',
|
||||
name: 'Enterprise',
|
||||
description: 'The perfect plan for enterprises',
|
||||
paymentType: 'recurring',
|
||||
currency: 'USD',
|
||||
paymentType: 'recurring',
|
||||
plans: [
|
||||
{
|
||||
id: 'enterprise-monthly',
|
||||
name: 'Enterprise Monthly',
|
||||
price: '99.99',
|
||||
interval: 'month',
|
||||
perSeat: false,
|
||||
id: 'enterprise-monthly',
|
||||
price: 99.99,
|
||||
recurring: {
|
||||
interval: 'month',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enterprise-yearly',
|
||||
name: 'Enterprise Yearly',
|
||||
price: '999.99',
|
||||
interval: 'year',
|
||||
perSeat: false,
|
||||
id: 'enterprise-yearly',
|
||||
price: 999.99,
|
||||
recurring: {
|
||||
interval: 'year',
|
||||
},
|
||||
},
|
||||
],
|
||||
features: [
|
||||
|
||||
Reference in New Issue
Block a user