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:
@@ -4,3 +4,4 @@ export * from './retrieve-checkout-session.schema';
|
||||
export * from './cancel-subscription-params.schema';
|
||||
export * from './report-billing-usage.schema';
|
||||
export * from './update-subscription-params.schema';
|
||||
export * from './query-billing-usage.schema';
|
||||
|
||||
@@ -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]),
|
||||
});
|
||||
@@ -1,9 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
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({
|
||||
quantity: z.number(),
|
||||
action: z.enum(['increment', 'set']),
|
||||
action: z.enum(['increment', 'set']).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
RetrieveCheckoutSessionSchema,
|
||||
UpdateSubscriptionParamsSchema,
|
||||
} from '../schema';
|
||||
import { QueryBillingUsageSchema } from '../schema/query-billing-usage.schema';
|
||||
|
||||
export abstract class BillingStrategyProviderService {
|
||||
abstract createBillingPortalSession(
|
||||
@@ -46,6 +47,12 @@ export abstract class BillingStrategyProviderService {
|
||||
success: boolean;
|
||||
}>;
|
||||
|
||||
abstract queryUsage(
|
||||
params: z.infer<typeof QueryBillingUsageSchema>,
|
||||
): Promise<{
|
||||
value: number;
|
||||
}>;
|
||||
|
||||
abstract updateSubscription(
|
||||
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||
): Promise<{
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
CancelSubscriptionParamsSchema,
|
||||
CreateBillingCheckoutSchema,
|
||||
CreateBillingPortalSessionSchema,
|
||||
QueryBillingUsageSchema,
|
||||
ReportBillingUsageSchema,
|
||||
RetrieveCheckoutSessionSchema,
|
||||
UpdateSubscriptionParamsSchema,
|
||||
@@ -118,6 +119,21 @@ class BillingGatewayService {
|
||||
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.
|
||||
* @param params
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
createUsageRecord,
|
||||
getCheckout,
|
||||
getVariant,
|
||||
listUsageRecords,
|
||||
updateSubscriptionItem,
|
||||
} from '@lemonsqueezy/lemonsqueezy.js';
|
||||
import { z } from 'zod';
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
CancelSubscriptionParamsSchema,
|
||||
CreateBillingCheckoutSchema,
|
||||
CreateBillingPortalSessionSchema,
|
||||
QueryBillingUsageSchema,
|
||||
ReportBillingUsageSchema,
|
||||
RetrieveCheckoutSessionSchema,
|
||||
UpdateSubscriptionParamsSchema,
|
||||
@@ -28,6 +30,11 @@ export class LemonSqueezyBillingStrategyService
|
||||
{
|
||||
private readonly namespace = 'billing.lemon-squeezy';
|
||||
|
||||
/**
|
||||
* @name createCheckoutSession
|
||||
* @description Creates a checkout session for a customer
|
||||
* @param params
|
||||
*/
|
||||
async createCheckoutSession(
|
||||
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(
|
||||
params: z.infer<typeof CreateBillingPortalSessionSchema>,
|
||||
) {
|
||||
@@ -95,6 +107,11 @@ export class LemonSqueezyBillingStrategyService
|
||||
return { url: data };
|
||||
}
|
||||
|
||||
/**
|
||||
* @name cancelSubscription
|
||||
* @description Cancels a subscription
|
||||
* @param params
|
||||
*/
|
||||
async cancelSubscription(
|
||||
params: z.infer<typeof CancelSubscriptionParamsSchema>,
|
||||
) {
|
||||
@@ -138,6 +155,11 @@ export class LemonSqueezyBillingStrategyService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @name retrieveCheckoutSession
|
||||
* @description Retrieves a checkout session
|
||||
* @param params
|
||||
*/
|
||||
async retrieveCheckoutSession(
|
||||
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>) {
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
name: this.namespace,
|
||||
subscriptionItemId: params.subscriptionItemId,
|
||||
subscriptionItemId: params.id,
|
||||
};
|
||||
|
||||
logger.info(ctx, 'Reporting usage...');
|
||||
|
||||
const { error } = await createUsageRecord({
|
||||
quantity: params.usage.quantity,
|
||||
subscriptionItemId: params.subscriptionItemId,
|
||||
subscriptionItemId: params.id,
|
||||
action: params.usage.action,
|
||||
});
|
||||
|
||||
@@ -211,6 +238,75 @@ export class LemonSqueezyBillingStrategyService
|
||||
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(
|
||||
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||
) {
|
||||
@@ -244,6 +340,11 @@ export class LemonSqueezyBillingStrategyService
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* @name queryUsage
|
||||
* @description Queries the usage of the metered billing
|
||||
* @param planId
|
||||
*/
|
||||
async getPlanById(planId: string) {
|
||||
const logger = await getLogger();
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CancelSubscriptionParamsSchema,
|
||||
CreateBillingCheckoutSchema,
|
||||
CreateBillingPortalSessionSchema,
|
||||
QueryBillingUsageSchema,
|
||||
ReportBillingUsageSchema,
|
||||
RetrieveCheckoutSessionSchema,
|
||||
UpdateSubscriptionParamsSchema,
|
||||
@@ -23,6 +24,11 @@ export class StripeBillingStrategyService
|
||||
{
|
||||
private readonly namespace = 'billing.stripe';
|
||||
|
||||
/**
|
||||
* @name createCheckoutSession
|
||||
* @description Creates a checkout session for a customer
|
||||
* @param params
|
||||
*/
|
||||
async createCheckoutSession(
|
||||
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
||||
) {
|
||||
@@ -50,6 +56,11 @@ export class StripeBillingStrategyService
|
||||
return { checkoutToken: client_secret };
|
||||
}
|
||||
|
||||
/**
|
||||
* @name createBillingPortalSession
|
||||
* @description Creates a billing portal session for a customer
|
||||
* @param params
|
||||
*/
|
||||
async createBillingPortalSession(
|
||||
params: z.infer<typeof CreateBillingPortalSessionSchema>,
|
||||
) {
|
||||
@@ -74,6 +85,11 @@ export class StripeBillingStrategyService
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name cancelSubscription
|
||||
* @description Cancels a subscription
|
||||
* @param params
|
||||
*/
|
||||
async cancelSubscription(
|
||||
params: z.infer<typeof CancelSubscriptionParamsSchema>,
|
||||
) {
|
||||
@@ -108,6 +124,11 @@ export class StripeBillingStrategyService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @name retrieveCheckoutSession
|
||||
* @description Retrieves a checkout session
|
||||
* @param params
|
||||
*/
|
||||
async retrieveCheckoutSession(
|
||||
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>) {
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
name: this.namespace,
|
||||
subscriptionItemId: params.subscriptionItemId,
|
||||
subscriptionItemId: params.id,
|
||||
usage: params.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 {
|
||||
await stripe.subscriptionItems.createUsageRecord(
|
||||
params.subscriptionItemId,
|
||||
{
|
||||
quantity: params.usage.quantity,
|
||||
action: params.usage.action,
|
||||
await stripe.billing.meterEvents.create({
|
||||
event_name: params.eventName,
|
||||
payload: {
|
||||
value: params.usage.quantity.toString(),
|
||||
stripe_customer_id: params.id,
|
||||
},
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
@@ -180,9 +212,71 @@ export class StripeBillingStrategyService
|
||||
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(
|
||||
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) {
|
||||
const logger = await getLogger();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user