Files
medreport_mrb2b/packages/billing/stripe/src/services/stripe-billing-strategy.service.ts
2025-06-08 16:18:30 +03:00

421 lines
10 KiB
TypeScript

import 'server-only';
import type { Stripe } from 'stripe';
import { z } from 'zod';
import { BillingStrategyProviderService } from '@kit/billing';
import type {
CancelSubscriptionParamsSchema,
CreateBillingCheckoutSchema,
CreateBillingPortalSessionSchema,
QueryBillingUsageSchema,
ReportBillingUsageSchema,
RetrieveCheckoutSessionSchema,
UpdateSubscriptionParamsSchema,
} from '@kit/billing/schema';
import { getLogger } from '@kit/shared/logger';
import { createStripeBillingPortalSession } from './create-stripe-billing-portal-session';
import { createStripeCheckout } from './create-stripe-checkout';
import { createStripeClient } from './stripe-sdk';
import { createStripeSubscriptionPayloadBuilderService } from './stripe-subscription-payload-builder.service';
/**
* @name StripeBillingStrategyService
* @description The Stripe billing strategy service
* @class StripeBillingStrategyService
* @implements {BillingStrategyProviderService}
*/
export class StripeBillingStrategyService
implements BillingStrategyProviderService
{
private readonly namespace = 'billing.stripe';
/**
* @name createCheckoutSession
* @description Creates a checkout session for a customer
* @param params
*/
async createCheckoutSession(
params: z.infer<typeof CreateBillingCheckoutSchema>,
) {
const stripe = await this.stripeProvider();
const logger = await getLogger();
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(ctx, 'Failed to create checkout session');
throw new Error('Failed to create checkout session');
}
logger.info(ctx, 'Checkout session created successfully');
return { checkoutToken: client_secret };
}
/**
* @name createBillingPortalSession
* @description Creates a billing portal session for a customer
* @param params
*/
async createBillingPortalSession(
params: z.infer<typeof CreateBillingPortalSessionSchema>,
) {
const stripe = await this.stripeProvider();
const logger = await getLogger();
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(ctx, 'Failed to create billing portal session');
} else {
logger.info(ctx, 'Billing portal session created successfully');
}
return session;
}
/**
* @name cancelSubscription
* @description Cancels a subscription
* @param params
*/
async cancelSubscription(
params: z.infer<typeof CancelSubscriptionParamsSchema>,
) {
const stripe = await this.stripeProvider();
const logger = await getLogger();
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(ctx, 'Subscription cancelled successfully');
return {
success: true,
};
} catch (error) {
logger.info(
{
...ctx,
error,
},
`Failed to cancel subscription. It may have already been cancelled on the user's end.`,
);
return {
success: false,
};
}
}
/**
* @name retrieveCheckoutSession
* @description Retrieves a checkout session
* @param params
*/
async retrieveCheckoutSession(
params: z.infer<typeof RetrieveCheckoutSessionSchema>,
) {
const stripe = await this.stripeProvider();
const logger = await getLogger();
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(ctx, 'Checkout session retrieved successfully');
return {
checkoutToken: session.client_secret,
isSessionOpen,
status: session.status ?? 'complete',
customer: {
email: session.customer_details?.email ?? null,
},
};
} catch (error) {
logger.error(
{
...ctx,
error,
},
'Failed to retrieve checkout session',
);
throw new Error('Failed to retrieve checkout session');
}
}
/**
* @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.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.billing.meterEvents.create({
event_name: params.eventName,
payload: {
value: params.usage.quantity.toString(),
stripe_customer_id: params.id,
},
});
} catch (error) {
logger.error(
{
...ctx,
error,
},
'Failed to report usage',
);
throw new Error('Failed to report usage');
}
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 updateSubscriptionItem
* @description Updates a subscription
* @param params
*/
async updateSubscriptionItem(
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
) {
const stripe = await this.stripeProvider();
const logger = await getLogger();
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, {
items: [
{
id: params.subscriptionItemId,
quantity: params.quantity,
},
],
});
logger.info(ctx, 'Subscription updated successfully');
return { success: true };
} catch (error) {
logger.error({ ...ctx, error }, 'Failed to update subscription');
throw new Error('Failed to update subscription');
}
}
/**
* @name getPlanById
* @description Retrieves a plan by id
* @param planId
*/
async getPlanById(planId: string) {
const logger = await getLogger();
const ctx = {
name: this.namespace,
planId,
};
logger.info(ctx, 'Retrieving plan by id...');
const stripe = await this.stripeProvider();
try {
const plan = await stripe.plans.retrieve(planId);
logger.info(ctx, 'Plan retrieved successfully');
return {
id: plan.id,
name: plan.nickname ?? '',
amount: plan.amount ?? 0,
interval: plan.interval,
};
} catch (error) {
logger.error({ ...ctx, error }, 'Failed to retrieve plan');
throw new Error('Failed to retrieve plan');
}
}
async getSubscription(subscriptionId: string) {
const stripe = await this.stripeProvider();
const logger = await getLogger();
const ctx = {
name: this.namespace,
subscriptionId,
};
logger.info(ctx, 'Retrieving subscription...');
const subscriptionPayloadBuilder =
createStripeSubscriptionPayloadBuilderService();
try {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
logger.info(ctx, 'Subscription retrieved successfully');
const customer = subscription.customer as string;
const accountId = subscription.metadata?.accountId as string;
const periodStartsAt =
subscriptionPayloadBuilder.getPeriodStartsAt(subscription);
const periodEndsAt =
subscriptionPayloadBuilder.getPeriodEndsAt(subscription);
const lineItems = subscription.items.data.map((item) => {
return {
...item,
// we cannot retrieve this from the API, user should retrieve from the billing configuration if needed
type: '' as never,
};
});
return subscriptionPayloadBuilder.build({
customerId: customer,
accountId,
id: subscription.id,
lineItems,
status: subscription.status,
currency: subscription.currency,
cancelAtPeriodEnd: subscription.cancel_at_period_end,
periodStartsAt,
periodEndsAt,
trialStartsAt: subscription.trial_start,
trialEndsAt: subscription.trial_end,
});
} catch (error) {
logger.error({ ...ctx, error }, 'Failed to retrieve subscription');
throw new Error('Failed to retrieve subscription');
}
}
private async stripeProvider(): Promise<Stripe> {
return createStripeClient();
}
}