Refactor logger usage and add discount field to checkout
This commit refactors the logger usage in various files to make it more streamlined and consistent. It also introduces a new 'enableDiscountField' feature for the checkout that can be toggled on specific products. This allows customers to apply discounts at checkout if the product or subscription plan has the 'enableDiscountField' set to true.
This commit is contained in:
@@ -17,6 +17,8 @@ import billingConfig from '~/config/billing.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
export class UserBillingService {
|
||||
private readonly namespace = 'billing.personal-account';
|
||||
|
||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||
|
||||
async createCheckoutSession({
|
||||
@@ -73,6 +75,7 @@ export class UserBillingService {
|
||||
customerId,
|
||||
plan,
|
||||
variantQuantities: [],
|
||||
enableDiscountField: product.enableDiscountField,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
@@ -111,6 +114,7 @@ export class UserBillingService {
|
||||
}
|
||||
|
||||
const service = await getBillingGatewayProvider(this.client);
|
||||
const logger = await getLogger();
|
||||
|
||||
const accountId = data.id;
|
||||
const customerId = await getCustomerIdFromAccountId(accountId);
|
||||
@@ -120,14 +124,14 @@ export class UserBillingService {
|
||||
throw new Error('Customer not found');
|
||||
}
|
||||
|
||||
const logger = await getLogger();
|
||||
const ctx = {
|
||||
name: this.namespace,
|
||||
customerId,
|
||||
accountId,
|
||||
};
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: `billing.personal-account`,
|
||||
customerId,
|
||||
accountId,
|
||||
},
|
||||
ctx,
|
||||
`User requested a Billing Portal session. Contacting provider...`,
|
||||
);
|
||||
|
||||
@@ -144,8 +148,7 @@ export class UserBillingService {
|
||||
logger.error(
|
||||
{
|
||||
error,
|
||||
customerId,
|
||||
accountId,
|
||||
...ctx,
|
||||
},
|
||||
`Failed to create a Billing Portal session`,
|
||||
);
|
||||
@@ -155,14 +158,7 @@ export class UserBillingService {
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: `billing.personal-account`,
|
||||
customerId,
|
||||
accountId,
|
||||
},
|
||||
`Session successfully created.`,
|
||||
);
|
||||
logger.info(ctx, `Session successfully created.`);
|
||||
|
||||
// redirect user to billing portal
|
||||
return url;
|
||||
|
||||
@@ -38,14 +38,13 @@ export class TeamBillingService {
|
||||
const accountId = params.accountId;
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info(
|
||||
{
|
||||
userId,
|
||||
accountId,
|
||||
name: this.namespace,
|
||||
},
|
||||
`Requested checkout session. Processing...`,
|
||||
);
|
||||
const ctx = {
|
||||
userId,
|
||||
accountId,
|
||||
name: this.namespace,
|
||||
};
|
||||
|
||||
logger.info(ctx, `Requested checkout session. Processing...`);
|
||||
|
||||
// verify permissions to manage billing
|
||||
const hasPermission = await getBillingPermissionsForAccountId(
|
||||
@@ -57,11 +56,7 @@ export class TeamBillingService {
|
||||
// then we should not proceed
|
||||
if (!hasPermission) {
|
||||
logger.warn(
|
||||
{
|
||||
userId,
|
||||
accountId,
|
||||
name: this.namespace,
|
||||
},
|
||||
ctx,
|
||||
`User without permissions attempted to create checkout.`,
|
||||
);
|
||||
|
||||
@@ -74,7 +69,7 @@ export class TeamBillingService {
|
||||
|
||||
// retrieve the plan from the configuration
|
||||
// so we can assign the correct checkout data
|
||||
const plan = getPlan(params.productId, params.planId);
|
||||
const { plan, product } = getPlanDetails(params.productId, params.planId);
|
||||
|
||||
// find the customer ID for the account if it exists
|
||||
// (eg. if the account has been billed before)
|
||||
@@ -94,10 +89,8 @@ export class TeamBillingService {
|
||||
|
||||
logger.info(
|
||||
{
|
||||
userId,
|
||||
accountId,
|
||||
...ctx,
|
||||
planId: plan.id,
|
||||
namespace: this.namespace,
|
||||
},
|
||||
`Creating checkout session...`,
|
||||
);
|
||||
@@ -111,6 +104,7 @@ export class TeamBillingService {
|
||||
customerEmail,
|
||||
customerId,
|
||||
variantQuantities,
|
||||
enableDiscountField: product.enableDiscountField,
|
||||
});
|
||||
|
||||
// return the checkout token to the client
|
||||
@@ -121,10 +115,8 @@ export class TeamBillingService {
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
name: this.namespace,
|
||||
...ctx,
|
||||
error,
|
||||
accountId,
|
||||
planId: plan.id,
|
||||
},
|
||||
`Error creating the checkout session`,
|
||||
);
|
||||
@@ -338,7 +330,7 @@ async function getCustomerIdFromAccountId(
|
||||
return data?.customer_id;
|
||||
}
|
||||
|
||||
function getPlan(productId: string, planId: string) {
|
||||
function getPlanDetails(productId: string, planId: string) {
|
||||
const product = billingConfig.products.find(
|
||||
(product) => product.id === productId,
|
||||
);
|
||||
@@ -353,5 +345,5 @@ function getPlan(productId: string, planId: string) {
|
||||
throw new Error('Plan not found');
|
||||
}
|
||||
|
||||
return plan;
|
||||
return { plan, product };
|
||||
}
|
||||
|
||||
@@ -167,7 +167,18 @@ const ProductSchema = z
|
||||
'Badge for the product. Displayed to the user. Example: "Popular"',
|
||||
})
|
||||
.optional(),
|
||||
features: z.array(z.string()).nonempty(),
|
||||
features: z
|
||||
.array(
|
||||
z.string({
|
||||
description: 'Features of the product. Displayed to the user.',
|
||||
}),
|
||||
)
|
||||
.nonempty(),
|
||||
enableDiscountField: z
|
||||
.boolean({
|
||||
description: 'Enable discount field for the product in the checkout.',
|
||||
})
|
||||
.optional(),
|
||||
highlighted: z
|
||||
.boolean({
|
||||
description: 'Highlight this product. Displayed to the user.',
|
||||
|
||||
@@ -9,6 +9,7 @@ export const CreateBillingCheckoutSchema = z.object({
|
||||
trialDays: z.number().optional(),
|
||||
customerId: z.string().optional(),
|
||||
customerEmail: z.string().email().optional(),
|
||||
enableDiscountField: z.boolean().optional(),
|
||||
variantQuantities: z.array(
|
||||
z.object({
|
||||
variantId: z.string().min(1),
|
||||
|
||||
@@ -153,6 +153,10 @@ export function PlanPicker(
|
||||
id={interval}
|
||||
value={interval}
|
||||
onClick={() => {
|
||||
form.setValue('interval', interval, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
if (selectedProduct) {
|
||||
const plan = selectedProduct.plans.find(
|
||||
(item) => item.interval === interval,
|
||||
@@ -160,12 +164,10 @@ export function PlanPicker(
|
||||
|
||||
form.setValue('planId', plan?.id ?? '', {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
}
|
||||
|
||||
form.setValue('interval', interval, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -202,7 +204,7 @@ export function PlanPicker(
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<RadioGroup name={field.name}>
|
||||
<RadioGroup value={field.value} name={field.name}>
|
||||
{props.config.products.map((product) => {
|
||||
const plan = product.plans.find((item) => {
|
||||
if (item.paymentType === 'one-time') {
|
||||
@@ -216,9 +218,12 @@ export function PlanPicker(
|
||||
return null;
|
||||
}
|
||||
|
||||
const planId = plan.id;
|
||||
const selected = field.value === planId;
|
||||
|
||||
const primaryLineItem = getPrimaryLineItem(
|
||||
props.config,
|
||||
plan.id,
|
||||
planId,
|
||||
);
|
||||
|
||||
if (!primaryLineItem) {
|
||||
@@ -227,14 +232,19 @@ export function PlanPicker(
|
||||
|
||||
return (
|
||||
<RadioGroupItemLabel
|
||||
selected={field.value === plan.id}
|
||||
key={plan.id}
|
||||
selected={selected}
|
||||
key={primaryLineItem.id}
|
||||
>
|
||||
<RadioGroupItem
|
||||
key={plan.id + selected}
|
||||
id={plan.id}
|
||||
value={plan.id}
|
||||
onClick={() => {
|
||||
form.setValue('planId', plan.id, {
|
||||
if (selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue('planId', planId, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ export async function createLemonSqueezyCheckout(
|
||||
embed: true,
|
||||
media: true,
|
||||
logo: true,
|
||||
discount: params.enableDiscountField ?? false,
|
||||
},
|
||||
checkoutData: {
|
||||
email: customerEmail,
|
||||
|
||||
@@ -76,6 +76,7 @@ export async function createStripeCheckout(
|
||||
|
||||
return stripe.checkout.sessions.create({
|
||||
mode,
|
||||
allow_promotion_codes: params.enableDiscountField,
|
||||
ui_mode: uiMode,
|
||||
line_items: lineItems,
|
||||
client_reference_id: clientReferenceId,
|
||||
|
||||
@@ -21,44 +21,31 @@ import { createStripeClient } from './stripe-sdk';
|
||||
export class StripeBillingStrategyService
|
||||
implements BillingStrategyProviderService
|
||||
{
|
||||
private readonly namespace = 'billing.stripe';
|
||||
|
||||
async createCheckoutSession(
|
||||
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
||||
) {
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
customerId: params.customerId,
|
||||
accountId: params.accountId,
|
||||
},
|
||||
'Creating checkout session...',
|
||||
);
|
||||
const ctx = {
|
||||
name: this.namespace,
|
||||
customerId: params.customerId,
|
||||
accountId: params.accountId,
|
||||
};
|
||||
|
||||
logger.info(ctx, 'Creating checkout session...');
|
||||
|
||||
const { client_secret } = await createStripeCheckout(stripe, params);
|
||||
|
||||
if (!client_secret) {
|
||||
logger.error(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
customerId: params.customerId,
|
||||
accountId: params.accountId,
|
||||
},
|
||||
'Failed to create checkout session',
|
||||
);
|
||||
logger.error(ctx, 'Failed to create checkout session');
|
||||
|
||||
throw new Error('Failed to create checkout session');
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
customerId: params.customerId,
|
||||
accountId: params.accountId,
|
||||
},
|
||||
'Checkout session created successfully',
|
||||
);
|
||||
logger.info(ctx, 'Checkout session created successfully');
|
||||
|
||||
return { checkoutToken: client_secret };
|
||||
}
|
||||
@@ -69,32 +56,19 @@ export class StripeBillingStrategyService
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
customerId: params.customerId,
|
||||
},
|
||||
'Creating billing portal session...',
|
||||
);
|
||||
const ctx = {
|
||||
name: this.namespace,
|
||||
customerId: params.customerId,
|
||||
};
|
||||
|
||||
logger.info(ctx, 'Creating billing portal session...');
|
||||
|
||||
const session = await createStripeBillingPortalSession(stripe, params);
|
||||
|
||||
if (!session?.url) {
|
||||
logger.error(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
customerId: params.customerId,
|
||||
},
|
||||
'Failed to create billing portal session',
|
||||
);
|
||||
logger.error(ctx, 'Failed to create billing portal session');
|
||||
} else {
|
||||
logger.info(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
customerId: params.customerId,
|
||||
},
|
||||
'Billing portal session created successfully',
|
||||
);
|
||||
logger.info(ctx, 'Billing portal session created successfully');
|
||||
}
|
||||
|
||||
return session;
|
||||
@@ -106,34 +80,26 @@ export class StripeBillingStrategyService
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
subscriptionId: params.subscriptionId,
|
||||
},
|
||||
'Cancelling subscription...',
|
||||
);
|
||||
const ctx = {
|
||||
name: this.namespace,
|
||||
subscriptionId: params.subscriptionId,
|
||||
};
|
||||
|
||||
logger.info(ctx, 'Cancelling subscription...');
|
||||
|
||||
try {
|
||||
await stripe.subscriptions.cancel(params.subscriptionId, {
|
||||
invoice_now: params.invoiceNow ?? true,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
subscriptionId: params.subscriptionId,
|
||||
},
|
||||
'Subscription cancelled successfully',
|
||||
);
|
||||
logger.info(ctx, 'Subscription cancelled successfully');
|
||||
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
subscriptionId: params.subscriptionId,
|
||||
error: e,
|
||||
...ctx,
|
||||
error,
|
||||
},
|
||||
'Failed to cancel subscription',
|
||||
);
|
||||
@@ -148,25 +114,18 @@ export class StripeBillingStrategyService
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
sessionId: params.sessionId,
|
||||
},
|
||||
'Retrieving checkout session...',
|
||||
);
|
||||
const ctx = {
|
||||
name: this.namespace,
|
||||
sessionId: params.sessionId,
|
||||
};
|
||||
|
||||
logger.info(ctx, 'Retrieving checkout session...');
|
||||
|
||||
try {
|
||||
const session = await stripe.checkout.sessions.retrieve(params.sessionId);
|
||||
const isSessionOpen = session.status === 'open';
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
sessionId: params.sessionId,
|
||||
},
|
||||
'Checkout session retrieved successfully',
|
||||
);
|
||||
logger.info(ctx, 'Checkout session retrieved successfully');
|
||||
|
||||
return {
|
||||
checkoutToken: session.client_secret,
|
||||
@@ -179,8 +138,7 @@ export class StripeBillingStrategyService
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
sessionId: params.sessionId,
|
||||
...ctx,
|
||||
error,
|
||||
},
|
||||
'Failed to retrieve checkout session',
|
||||
@@ -192,14 +150,35 @@ export class StripeBillingStrategyService
|
||||
|
||||
async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) {
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
await stripe.subscriptionItems.createUsageRecord(
|
||||
params.subscriptionItemId,
|
||||
{
|
||||
quantity: params.usage.quantity,
|
||||
action: params.usage.action,
|
||||
},
|
||||
);
|
||||
const ctx = {
|
||||
name: this.namespace,
|
||||
subscriptionItemId: params.subscriptionItemId,
|
||||
usage: params.usage,
|
||||
};
|
||||
|
||||
logger.info(ctx, 'Reporting usage...');
|
||||
|
||||
try {
|
||||
await stripe.subscriptionItems.createUsageRecord(
|
||||
params.subscriptionItemId,
|
||||
{
|
||||
quantity: params.usage.quantity,
|
||||
action: params.usage.action,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
...ctx,
|
||||
error,
|
||||
},
|
||||
'Failed to report usage',
|
||||
);
|
||||
|
||||
throw new Error('Failed to report usage');
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
@@ -210,13 +189,14 @@ export class StripeBillingStrategyService
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
...params,
|
||||
},
|
||||
'Updating subscription...',
|
||||
);
|
||||
const ctx = {
|
||||
name: this.namespace,
|
||||
subscriptionId: params.subscriptionId,
|
||||
subscriptionItemId: params.subscriptionItemId,
|
||||
quantity: params.quantity,
|
||||
};
|
||||
|
||||
logger.info(ctx, 'Updating subscription...');
|
||||
|
||||
try {
|
||||
await stripe.subscriptions.update(params.subscriptionId, {
|
||||
@@ -228,24 +208,11 @@ export class StripeBillingStrategyService
|
||||
],
|
||||
});
|
||||
|
||||
logger.info(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
...params,
|
||||
},
|
||||
'Subscription updated successfully',
|
||||
);
|
||||
logger.info(ctx, 'Subscription updated successfully');
|
||||
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
{
|
||||
name: 'billing.stripe',
|
||||
...params,
|
||||
error: e,
|
||||
},
|
||||
'Failed to update subscription',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error({ ...ctx, error }, 'Failed to update subscription');
|
||||
|
||||
throw new Error('Failed to update subscription');
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export function MultiFactorAuthFactorsList() {
|
||||
if (!allFactors.length) {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<Alert variant={'info'}>
|
||||
<Alert>
|
||||
<ShieldCheck className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
|
||||
Reference in New Issue
Block a user