Improve billing plan lookup (#270)
Retrieve plan from Stripe/LS if not found in billing configuration. Useful for legacy plans.
This commit is contained in:
committed by
GitHub
parent
2b21b7bed4
commit
856e9612c4
@@ -1,6 +1,6 @@
|
||||
import { BadgeCheck } from 'lucide-react';
|
||||
|
||||
import { BillingConfig, getProductPlanPairByVariantId } from '@kit/billing';
|
||||
import { PlanSchema, type ProductSchema } from '@kit/billing';
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import {
|
||||
Card,
|
||||
@@ -22,12 +22,14 @@ interface Props {
|
||||
items: LineItem[];
|
||||
};
|
||||
|
||||
config: BillingConfig;
|
||||
product: ProductSchema;
|
||||
plan: ReturnType<(typeof PlanSchema)['parse']>;
|
||||
}
|
||||
|
||||
export function CurrentLifetimeOrderCard({
|
||||
order,
|
||||
config,
|
||||
product,
|
||||
plan,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const lineItems = order.items;
|
||||
const firstLineItem = lineItems[0];
|
||||
@@ -36,17 +38,6 @@ export function CurrentLifetimeOrderCard({
|
||||
throw new Error('No line items found in subscription');
|
||||
}
|
||||
|
||||
const { product, plan } = getProductPlanPairByVariantId(
|
||||
config,
|
||||
firstLineItem.variant_id,
|
||||
);
|
||||
|
||||
if (!product || !plan) {
|
||||
throw new Error(
|
||||
'Product or plan not found. Did you forget to add it to the billing config?',
|
||||
);
|
||||
}
|
||||
|
||||
const productLineItems = plan.lineItems;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatDate } from 'date-fns';
|
||||
import { BadgeCheck } from 'lucide-react';
|
||||
|
||||
import { BillingConfig, getProductPlanPairByVariantId } from '@kit/billing';
|
||||
import { PlanSchema, type ProductSchema } from '@kit/billing';
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -26,12 +26,14 @@ interface Props {
|
||||
items: LineItem[];
|
||||
};
|
||||
|
||||
config: BillingConfig;
|
||||
product: ProductSchema;
|
||||
plan: ReturnType<(typeof PlanSchema)['parse']>;
|
||||
}
|
||||
|
||||
export function CurrentSubscriptionCard({
|
||||
subscription,
|
||||
config,
|
||||
product,
|
||||
plan,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const lineItems = subscription.items;
|
||||
const firstLineItem = lineItems[0];
|
||||
@@ -40,17 +42,6 @@ export function CurrentSubscriptionCard({
|
||||
throw new Error('No line items found in subscription');
|
||||
}
|
||||
|
||||
const { product, plan } = getProductPlanPairByVariantId(
|
||||
config,
|
||||
firstLineItem.variant_id,
|
||||
);
|
||||
|
||||
if (!product || !plan) {
|
||||
throw new Error(
|
||||
'Product or plan not found. Did you forget to add it to the billing config?',
|
||||
);
|
||||
}
|
||||
|
||||
const productLineItems = plan.lineItems;
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './server/services/billing-gateway/billing-gateway.service';
|
||||
export * from './server/services/billing-gateway/billing-gateway-provider-factory';
|
||||
export * from './server/services/billing-event-handler/billing-event-handler-provider';
|
||||
export * from './server/services/billing-webhooks/billing-webhooks.service';
|
||||
export * from './server/utils/resolve-product-plan';
|
||||
|
||||
@@ -114,6 +114,16 @@ class BillingGatewayService {
|
||||
return strategy.queryUsage(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves plan details from the billing provider.
|
||||
* @param planId - The identifier of the plan on the provider side.
|
||||
*/
|
||||
async getPlanById(planId: string) {
|
||||
const strategy = await this.getStrategy();
|
||||
|
||||
return strategy.getPlanById(planId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a subscription with the specified parameters.
|
||||
* @param params
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
BillingConfig,
|
||||
LineItemSchema,
|
||||
PlanSchema,
|
||||
type ProductSchema,
|
||||
getProductPlanPairByVariantId,
|
||||
} from '@kit/billing';
|
||||
import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
/**
|
||||
* @name resolveProductPlan
|
||||
* @description
|
||||
* Tries to find a product and plan in the local billing config.
|
||||
* Falls back to fetching the plan details from the billing provider if missing.
|
||||
*/
|
||||
export async function resolveProductPlan(
|
||||
config: BillingConfig,
|
||||
variantId: string,
|
||||
currency: string,
|
||||
): Promise<{
|
||||
product: ProductSchema;
|
||||
plan: z.infer<typeof PlanSchema>;
|
||||
}> {
|
||||
// we can't always guarantee that the plan will be present in the local config
|
||||
// so we need to fallback to fetching the plan details from the billing provider
|
||||
try {
|
||||
// attempt to get the plan details from the local config
|
||||
return getProductPlanPairByVariantId(config, variantId);
|
||||
} catch {
|
||||
// retrieve the plan details from the billing provider
|
||||
return fetchPlanDetailsFromProvider({ variantId, currency });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @name fetchPlanDetailsFromProvider
|
||||
* @description
|
||||
* Fetches the plan details from the billing provider
|
||||
* @param variantId - The variant ID of the plan
|
||||
* @param currency - The currency of the plan
|
||||
* @returns The product and plan objects
|
||||
*/
|
||||
async function fetchPlanDetailsFromProvider({
|
||||
variantId,
|
||||
currency,
|
||||
}: {
|
||||
variantId: string;
|
||||
currency: string;
|
||||
}) {
|
||||
const client = getSupabaseServerClient();
|
||||
const gateway = await getBillingGatewayProvider(client);
|
||||
|
||||
const providerPlan = await gateway.getPlanById(variantId);
|
||||
|
||||
const plan = PlanSchema.parse({
|
||||
id: providerPlan.id,
|
||||
name: providerPlan.name,
|
||||
description: providerPlan.description ?? providerPlan.name,
|
||||
interval: providerPlan.interval as 'month' | 'year' | undefined,
|
||||
paymentType: providerPlan.type,
|
||||
lineItems: [
|
||||
LineItemSchema.parse({
|
||||
id: providerPlan.id,
|
||||
name: providerPlan.name,
|
||||
cost: providerPlan.amount,
|
||||
// support only flat plans - tiered plans are not supported
|
||||
// however, users can clarify the plan details in the description
|
||||
type: 'flat',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// create a minimal product and plan object so we can
|
||||
// display the plan details in the UI
|
||||
const product: ProductSchema = {
|
||||
id: providerPlan.id,
|
||||
name: providerPlan.name,
|
||||
description: providerPlan.description ?? '',
|
||||
currency,
|
||||
features: [''],
|
||||
plans: [],
|
||||
};
|
||||
|
||||
return { product, plan };
|
||||
}
|
||||
Reference in New Issue
Block a user