Add queryUsage method to billing services

A new method, queryUsage, has been added to the billing strategy classes and the gateway service to offer usage querying capabilities for Stripe and Lemon Squeezy. QueryBillingUsageSchema has also been introduced in the schema-related changes. Changes also include updates to the functions' comment descriptions. A minor tweak was made to the Button and Link components in the `faq/page.tsx' file.
This commit is contained in:
giancarlo
2024-04-25 11:34:47 +07:00
parent 19332d124d
commit 8d04624b1d
8 changed files with 278 additions and 14 deletions

View File

@@ -86,8 +86,8 @@ async function FAQPage() {
</div> </div>
<div> <div>
<Button variant={'outline'}> <Button asChild variant={'outline'}>
<Link asChild href={'/contact'}> <Link href={'/contact'}>
<span> <span>
<Trans i18nKey={'marketing:contactFaq'} /> <Trans i18nKey={'marketing:contactFaq'} />
</span> </span>

View File

@@ -4,3 +4,4 @@ export * from './retrieve-checkout-session.schema';
export * from './cancel-subscription-params.schema'; export * from './cancel-subscription-params.schema';
export * from './report-billing-usage.schema'; export * from './report-billing-usage.schema';
export * from './update-subscription-params.schema'; export * from './update-subscription-params.schema';
export * from './query-billing-usage.schema';

View File

@@ -0,0 +1,32 @@
import { z } from 'zod';
const TimeFilter = z.object(
{
startTime: z.number(),
endTime: z.number(),
},
{
description: `The time range to filter the usage records. Used for Stripe`,
},
);
const PageFilter = z.object(
{
page: z.number(),
size: z.number(),
},
{
description: `The page and size to filter the usage records. Used for LS`,
},
);
export const QueryBillingUsageSchema = z.object({
id: z.string({
description:
'The id of the usage record. For Stripe a meter ID, for LS a subscription item ID.',
}),
customerId: z.string({
description: 'The id of the customer in the billing system',
}),
filter: z.union([TimeFilter, PageFilter]),
});

View File

@@ -1,9 +1,17 @@
import { z } from 'zod'; import { z } from 'zod';
export const ReportBillingUsageSchema = z.object({ export const ReportBillingUsageSchema = z.object({
subscriptionItemId: z.string(), id: z.string({
description:
'The id of the usage record. For Stripe a customer ID, for LS a subscription item ID.',
}),
eventName: z
.string({
description: 'The name of the event that triggered the usage',
})
.optional(),
usage: z.object({ usage: z.object({
quantity: z.number(), quantity: z.number(),
action: z.enum(['increment', 'set']), action: z.enum(['increment', 'set']).optional(),
}), }),
}); });

View File

@@ -8,6 +8,7 @@ import {
RetrieveCheckoutSessionSchema, RetrieveCheckoutSessionSchema,
UpdateSubscriptionParamsSchema, UpdateSubscriptionParamsSchema,
} from '../schema'; } from '../schema';
import { QueryBillingUsageSchema } from '../schema/query-billing-usage.schema';
export abstract class BillingStrategyProviderService { export abstract class BillingStrategyProviderService {
abstract createBillingPortalSession( abstract createBillingPortalSession(
@@ -46,6 +47,12 @@ export abstract class BillingStrategyProviderService {
success: boolean; success: boolean;
}>; }>;
abstract queryUsage(
params: z.infer<typeof QueryBillingUsageSchema>,
): Promise<{
value: number;
}>;
abstract updateSubscription( abstract updateSubscription(
params: z.infer<typeof UpdateSubscriptionParamsSchema>, params: z.infer<typeof UpdateSubscriptionParamsSchema>,
): Promise<{ ): Promise<{

View File

@@ -7,6 +7,7 @@ import {
CancelSubscriptionParamsSchema, CancelSubscriptionParamsSchema,
CreateBillingCheckoutSchema, CreateBillingCheckoutSchema,
CreateBillingPortalSessionSchema, CreateBillingPortalSessionSchema,
QueryBillingUsageSchema,
ReportBillingUsageSchema, ReportBillingUsageSchema,
RetrieveCheckoutSessionSchema, RetrieveCheckoutSessionSchema,
UpdateSubscriptionParamsSchema, UpdateSubscriptionParamsSchema,
@@ -118,6 +119,21 @@ class BillingGatewayService {
return strategy.reportUsage(payload); return strategy.reportUsage(payload);
} }
/**
* @name queryUsage
* @description Queries the usage of the metered billing.
* @param params
*/
async queryUsage(params: z.infer<typeof QueryBillingUsageSchema>) {
const strategy = await BillingGatewayFactoryService.GetProviderStrategy(
this.provider,
);
const payload = QueryBillingUsageSchema.parse(params);
return strategy.queryUsage(payload);
}
/** /**
* Updates a subscription with the specified parameters. * Updates a subscription with the specified parameters.
* @param params * @param params

View File

@@ -5,6 +5,7 @@ import {
createUsageRecord, createUsageRecord,
getCheckout, getCheckout,
getVariant, getVariant,
listUsageRecords,
updateSubscriptionItem, updateSubscriptionItem,
} from '@lemonsqueezy/lemonsqueezy.js'; } from '@lemonsqueezy/lemonsqueezy.js';
import { z } from 'zod'; import { z } from 'zod';
@@ -14,6 +15,7 @@ import {
CancelSubscriptionParamsSchema, CancelSubscriptionParamsSchema,
CreateBillingCheckoutSchema, CreateBillingCheckoutSchema,
CreateBillingPortalSessionSchema, CreateBillingPortalSessionSchema,
QueryBillingUsageSchema,
ReportBillingUsageSchema, ReportBillingUsageSchema,
RetrieveCheckoutSessionSchema, RetrieveCheckoutSessionSchema,
UpdateSubscriptionParamsSchema, UpdateSubscriptionParamsSchema,
@@ -28,6 +30,11 @@ export class LemonSqueezyBillingStrategyService
{ {
private readonly namespace = 'billing.lemon-squeezy'; private readonly namespace = 'billing.lemon-squeezy';
/**
* @name createCheckoutSession
* @description Creates a checkout session for a customer
* @param params
*/
async createCheckoutSession( async createCheckoutSession(
params: z.infer<typeof CreateBillingCheckoutSchema>, params: z.infer<typeof CreateBillingCheckoutSchema>,
) { ) {
@@ -63,6 +70,11 @@ export class LemonSqueezyBillingStrategyService
}; };
} }
/**
* @name createBillingPortalSession
* @description Creates a billing portal session for a customer
* @param params
*/
async createBillingPortalSession( async createBillingPortalSession(
params: z.infer<typeof CreateBillingPortalSessionSchema>, params: z.infer<typeof CreateBillingPortalSessionSchema>,
) { ) {
@@ -95,6 +107,11 @@ export class LemonSqueezyBillingStrategyService
return { url: data }; return { url: data };
} }
/**
* @name cancelSubscription
* @description Cancels a subscription
* @param params
*/
async cancelSubscription( async cancelSubscription(
params: z.infer<typeof CancelSubscriptionParamsSchema>, params: z.infer<typeof CancelSubscriptionParamsSchema>,
) { ) {
@@ -138,6 +155,11 @@ export class LemonSqueezyBillingStrategyService
} }
} }
/**
* @name retrieveCheckoutSession
* @description Retrieves a checkout session
* @param params
*/
async retrieveCheckoutSession( async retrieveCheckoutSession(
params: z.infer<typeof RetrieveCheckoutSessionSchema>, params: z.infer<typeof RetrieveCheckoutSessionSchema>,
) { ) {
@@ -178,19 +200,24 @@ export class LemonSqueezyBillingStrategyService
}; };
} }
/**
* @name reportUsage
* @description Reports the usage of the billing
* @param params
*/
async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) { async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) {
const logger = await getLogger(); const logger = await getLogger();
const ctx = { const ctx = {
name: this.namespace, name: this.namespace,
subscriptionItemId: params.subscriptionItemId, subscriptionItemId: params.id,
}; };
logger.info(ctx, 'Reporting usage...'); logger.info(ctx, 'Reporting usage...');
const { error } = await createUsageRecord({ const { error } = await createUsageRecord({
quantity: params.usage.quantity, quantity: params.usage.quantity,
subscriptionItemId: params.subscriptionItemId, subscriptionItemId: params.id,
action: params.usage.action, action: params.usage.action,
}); });
@@ -211,6 +238,75 @@ export class LemonSqueezyBillingStrategyService
return { success: true }; return { success: true };
} }
/**
* @name queryUsage
* @description Queries the usage of the metered billing
* @param params
*/
async queryUsage(
params: z.infer<typeof QueryBillingUsageSchema>,
): Promise<{ value: number }> {
const logger = await getLogger();
const ctx = {
name: this.namespace,
...params,
};
if (!('page' in params.filter)) {
logger.error(ctx, `Page parameters are required for Lemon Squeezy`);
throw new Error('Page is required');
}
logger.info(ctx, 'Querying usage...');
const records = await listUsageRecords({
filter: {
subscriptionItemId: params.id,
},
page: params.filter,
});
if (records.error) {
logger.error(
{
...ctx,
error: records.error,
},
'Failed to query usage',
);
throw new Error('Failed to query usage');
}
if (!records.data) {
return {
value: 0,
};
}
const value = records.data.data.reduce(
(acc, record) => acc + record.attributes.quantity,
0,
);
logger.info(
{
...ctx,
value,
},
'Usage queried successfully',
);
return { value };
}
/**
* @name queryUsage
* @description Queries the usage of the metered billing
* @param params
*/
async updateSubscription( async updateSubscription(
params: z.infer<typeof UpdateSubscriptionParamsSchema>, params: z.infer<typeof UpdateSubscriptionParamsSchema>,
) { ) {
@@ -244,6 +340,11 @@ export class LemonSqueezyBillingStrategyService
return { success: true }; return { success: true };
} }
/**
* @name queryUsage
* @description Queries the usage of the metered billing
* @param planId
*/
async getPlanById(planId: string) { async getPlanById(planId: string) {
const logger = await getLogger(); const logger = await getLogger();

View File

@@ -8,6 +8,7 @@ import {
CancelSubscriptionParamsSchema, CancelSubscriptionParamsSchema,
CreateBillingCheckoutSchema, CreateBillingCheckoutSchema,
CreateBillingPortalSessionSchema, CreateBillingPortalSessionSchema,
QueryBillingUsageSchema,
ReportBillingUsageSchema, ReportBillingUsageSchema,
RetrieveCheckoutSessionSchema, RetrieveCheckoutSessionSchema,
UpdateSubscriptionParamsSchema, UpdateSubscriptionParamsSchema,
@@ -23,6 +24,11 @@ export class StripeBillingStrategyService
{ {
private readonly namespace = 'billing.stripe'; private readonly namespace = 'billing.stripe';
/**
* @name createCheckoutSession
* @description Creates a checkout session for a customer
* @param params
*/
async createCheckoutSession( async createCheckoutSession(
params: z.infer<typeof CreateBillingCheckoutSchema>, params: z.infer<typeof CreateBillingCheckoutSchema>,
) { ) {
@@ -50,6 +56,11 @@ export class StripeBillingStrategyService
return { checkoutToken: client_secret }; return { checkoutToken: client_secret };
} }
/**
* @name createBillingPortalSession
* @description Creates a billing portal session for a customer
* @param params
*/
async createBillingPortalSession( async createBillingPortalSession(
params: z.infer<typeof CreateBillingPortalSessionSchema>, params: z.infer<typeof CreateBillingPortalSessionSchema>,
) { ) {
@@ -74,6 +85,11 @@ export class StripeBillingStrategyService
return session; return session;
} }
/**
* @name cancelSubscription
* @description Cancels a subscription
* @param params
*/
async cancelSubscription( async cancelSubscription(
params: z.infer<typeof CancelSubscriptionParamsSchema>, params: z.infer<typeof CancelSubscriptionParamsSchema>,
) { ) {
@@ -108,6 +124,11 @@ export class StripeBillingStrategyService
} }
} }
/**
* @name retrieveCheckoutSession
* @description Retrieves a checkout session
* @param params
*/
async retrieveCheckoutSession( async retrieveCheckoutSession(
params: z.infer<typeof RetrieveCheckoutSessionSchema>, params: z.infer<typeof RetrieveCheckoutSessionSchema>,
) { ) {
@@ -148,26 +169,37 @@ export class StripeBillingStrategyService
} }
} }
/**
* @name reportUsage
* @description Reports usage for a subscription with the Metrics API
* @param params
*/
async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) { async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) {
const stripe = await this.stripeProvider(); const stripe = await this.stripeProvider();
const logger = await getLogger(); const logger = await getLogger();
const ctx = { const ctx = {
name: this.namespace, name: this.namespace,
subscriptionItemId: params.subscriptionItemId, subscriptionItemId: params.id,
usage: params.usage, usage: params.usage,
}; };
logger.info(ctx, 'Reporting usage...'); logger.info(ctx, 'Reporting usage...');
if (!params.eventName) {
logger.error(ctx, 'Event name is required');
throw new Error('Event name is required when reporting Metrics');
}
try { try {
await stripe.subscriptionItems.createUsageRecord( await stripe.billing.meterEvents.create({
params.subscriptionItemId, event_name: params.eventName,
{ payload: {
quantity: params.usage.quantity, value: params.usage.quantity.toString(),
action: params.usage.action, stripe_customer_id: params.id,
}, },
); });
} catch (error) { } catch (error) {
logger.error( logger.error(
{ {
@@ -180,9 +212,71 @@ export class StripeBillingStrategyService
throw new Error('Failed to report usage'); throw new Error('Failed to report usage');
} }
return { success: true }; return {
success: true,
};
} }
/**
* @name queryUsage
* @description Reports the total usage for a subscription with the Metrics API
*/
async queryUsage(params: z.infer<typeof QueryBillingUsageSchema>) {
const stripe = await this.stripeProvider();
const logger = await getLogger();
const ctx = {
name: this.namespace,
id: params.id,
customerId: params.customerId,
};
// validate shape of filters for Stripe
if (!('startTime' in params.filter)) {
logger.error(ctx, 'Start and end time are required for Stripe');
throw new Error('Start and end time are required when querying usage');
}
logger.info(ctx, 'Querying billing usage...');
try {
const summaries = await stripe.billing.meters.listEventSummaries(
params.id,
{
customer: params.customerId,
start_time: params.filter.startTime,
end_time: params.filter.endTime,
},
);
logger.info(ctx, 'Billing usage queried successfully');
const value = summaries.data.reduce((acc, summary) => {
return acc + Number(summary.aggregated_value);
}, 0);
return {
value,
};
} catch (error) {
logger.error(
{
...ctx,
error,
},
'Failed to report usage',
);
throw new Error('Failed to report usage');
}
}
/**
* @name updateSubscription
* @description Updates a subscription
* @param params
*/
async updateSubscription( async updateSubscription(
params: z.infer<typeof UpdateSubscriptionParamsSchema>, params: z.infer<typeof UpdateSubscriptionParamsSchema>,
) { ) {
@@ -218,6 +312,11 @@ export class StripeBillingStrategyService
} }
} }
/**
* @name getPlanById
* @description Retrieves a plan by id
* @param planId
*/
async getPlanById(planId: string) { async getPlanById(planId: string) {
const logger = await getLogger(); const logger = await getLogger();