From 5b837d2fa81b73e36a8ae8537158f09e3f7afd52 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Thu, 11 Apr 2024 10:29:52 +0800 Subject: [PATCH] Update UI style and enhance billing services Several changes have been made in this commit. Firstly, updates have been made to the site-header-account-section and the pricing-table components to enhance UI aesthetics. Secondly, billing services have been significantly improved. A new method for retrieving plan information by an ID has been introduced. This method is available for all strategy services, including Stripe and Lemon-Squeezy. Furthermore, the way context and logging are handled during the billing process has been streamlined for better readability and efficiency. --- .../site-header-account-section.tsx | 2 +- .../billing-strategy-provider.service.ts | 7 + .../gateway/src/components/pricing-table.tsx | 2 +- .../billing-gateway.service.ts | 12 ++ .../lemon-squeezy-billing-strategy.service.ts | 179 +++++++++--------- .../stripe-billing-strategy.service.ts | 30 +++ 6 files changed, 144 insertions(+), 88 deletions(-) diff --git a/apps/web/app/(marketing)/_components/site-header-account-section.tsx b/apps/web/app/(marketing)/_components/site-header-account-section.tsx index 19352b5ef..ba53860b4 100644 --- a/apps/web/app/(marketing)/_components/site-header-account-section.tsx +++ b/apps/web/app/(marketing)/_components/site-header-account-section.tsx @@ -61,7 +61,7 @@ function AuthButtons() { - diff --git a/packages/billing/core/src/services/billing-strategy-provider.service.ts b/packages/billing/core/src/services/billing-strategy-provider.service.ts index b86a4ee59..01665e408 100644 --- a/packages/billing/core/src/services/billing-strategy-provider.service.ts +++ b/packages/billing/core/src/services/billing-strategy-provider.service.ts @@ -51,4 +51,11 @@ export abstract class BillingStrategyProviderService { ): Promise<{ success: boolean; }>; + + abstract getPlanById(planId: string): Promise<{ + id: string; + name: string; + interval: string; + amount: number; + }>; } diff --git a/packages/billing/gateway/src/components/pricing-table.tsx b/packages/billing/gateway/src/components/pricing-table.tsx index 8ac297d3d..d723a3ad0 100644 --- a/packages/billing/gateway/src/components/pricing-table.tsx +++ b/packages/billing/gateway/src/components/pricing-table.tsx @@ -153,7 +153,7 @@ function PricingItem( className={cn( props.className, `s-full flex flex-1 grow flex-col items-stretch justify-between space-y-8 self-stretch - rounded-lg p-6 ring-2 lg:w-4/12 xl:max-w-[22rem] xl:p-8`, + rounded-lg p-6 ring-2 lg:w-4/12 xl:max-w-[19rem]`, { ['ring-primary']: highlighted, ['dark:shadow-primary/30 shadow-none ring-transparent dark:shadow-sm']: diff --git a/packages/billing/gateway/src/server/services/billing-gateway/billing-gateway.service.ts b/packages/billing/gateway/src/server/services/billing-gateway/billing-gateway.service.ts index 84ac7e060..62a5f34fa 100644 --- a/packages/billing/gateway/src/server/services/billing-gateway/billing-gateway.service.ts +++ b/packages/billing/gateway/src/server/services/billing-gateway/billing-gateway.service.ts @@ -127,4 +127,16 @@ export class BillingGatewayService { return strategy.updateSubscription(payload); } + + /** + * Retrieves a plan by the specified plan ID. + * @param planId + */ + async getPlanById(planId: string) { + const strategy = await BillingGatewayFactoryService.GetProviderStrategy( + this.provider, + ); + + return strategy.getPlanById(planId); + } } diff --git a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts index 224e1a731..44a871e1f 100644 --- a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts +++ b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts @@ -4,6 +4,7 @@ import { cancelSubscription, createUsageRecord, getCheckout, + getVariant, updateSubscriptionItem, } from '@lemonsqueezy/lemonsqueezy.js'; import { z } from 'zod'; @@ -25,18 +26,19 @@ import { createLemonSqueezyCheckout } from './create-lemon-squeezy-checkout'; export class LemonSqueezyBillingStrategyService implements BillingStrategyProviderService { + private readonly namespace = 'billing.lemon-squeezy'; + async createCheckoutSession( params: z.infer, ) { const logger = await getLogger(); - logger.info( - { - name: 'billing.lemon-squeezy', - ...params, - }, - 'Creating checkout session...', - ); + const ctx = { + name: this.namespace, + ...params, + }; + + logger.info(ctx, 'Creating checkout session...'); const { data: response, error } = await createLemonSqueezyCheckout(params); @@ -45,9 +47,7 @@ export class LemonSqueezyBillingStrategyService logger.error( { - name: 'billing.lemon-squeezy', - customerId: params.customerId, - accountId: params.accountId, + ...ctx, error: error?.message, }, 'Failed to create checkout session', @@ -56,14 +56,7 @@ export class LemonSqueezyBillingStrategyService throw new Error('Failed to create checkout session'); } - logger.info( - { - name: 'billing.lemon-squeezy', - customerId: params.customerId, - accountId: params.accountId, - }, - 'Checkout session created successfully', - ); + logger.info(ctx, 'Checkout session created successfully'); return { checkoutToken: response.data.attributes.url, @@ -75,13 +68,12 @@ export class LemonSqueezyBillingStrategyService ) { const logger = await getLogger(); - logger.info( - { - name: 'billing.lemon-squeezy', - customerId: params.customerId, - }, - 'Creating billing portal session...', - ); + const ctx = { + name: this.namespace, + ...params, + }; + + logger.info(ctx, 'Creating billing portal session...'); const { data, error } = await createLemonSqueezyBillingPortalSession(params); @@ -89,8 +81,7 @@ export class LemonSqueezyBillingStrategyService if (error ?? !data) { logger.error( { - name: 'billing.lemon-squeezy', - customerId: params.customerId, + ...ctx, error: error?.message, }, 'Failed to create billing portal session', @@ -99,13 +90,7 @@ export class LemonSqueezyBillingStrategyService throw new Error('Failed to create billing portal session'); } - logger.info( - { - name: 'billing.lemon-squeezy', - customerId: params.customerId, - }, - 'Billing portal session created successfully', - ); + logger.info(ctx, 'Billing portal session created successfully'); return { url: data }; } @@ -115,13 +100,12 @@ export class LemonSqueezyBillingStrategyService ) { const logger = await getLogger(); - logger.info( - { - name: 'billing.lemon-squeezy', - subscriptionId: params.subscriptionId, - }, - 'Cancelling subscription...', - ); + const ctx = { + name: this.namespace, + subscriptionId: params.subscriptionId, + }; + + logger.info(ctx, 'Cancelling subscription...'); try { const { error } = await cancelSubscription(params.subscriptionId); @@ -129,8 +113,7 @@ export class LemonSqueezyBillingStrategyService if (error) { logger.error( { - name: 'billing.lemon-squeezy', - subscriptionId: params.subscriptionId, + ...ctx, error: error.message, }, 'Failed to cancel subscription', @@ -139,20 +122,13 @@ export class LemonSqueezyBillingStrategyService throw error; } - logger.info( - { - name: 'billing.lemon-squeezy', - subscriptionId: params.subscriptionId, - }, - 'Subscription cancelled successfully', - ); + logger.info(ctx, 'Subscription cancelled successfully'); return { success: true }; } catch (error) { logger.error( { - name: 'billing.lemon-squeezy', - subscriptionId: params.subscriptionId, + ...ctx, error: (error as Error)?.message, }, 'Failed to cancel subscription', @@ -167,21 +143,19 @@ export class LemonSqueezyBillingStrategyService ) { const logger = await getLogger(); - logger.info( - { - name: 'billing.lemon-squeezy', - sessionId: params.sessionId, - }, - 'Retrieving checkout session...', - ); + const ctx = { + name: this.namespace, + sessionId: params.sessionId, + }; + + logger.info(ctx, 'Retrieving checkout session...'); const { data: session, error } = await getCheckout(params.sessionId); if (error ?? !session?.data) { logger.error( { - name: 'billing.lemon-squeezy', - sessionId: params.sessionId, + ...ctx, error: error?.message, }, 'Failed to retrieve checkout session', @@ -190,13 +164,7 @@ export class LemonSqueezyBillingStrategyService throw new Error('Failed to retrieve checkout session'); } - logger.info( - { - name: 'billing.lemon-squeezy', - sessionId: params.sessionId, - }, - 'Checkout session retrieved successfully', - ); + logger.info(ctx, 'Checkout session retrieved successfully'); const { id, attributes } = session.data; @@ -213,13 +181,12 @@ export class LemonSqueezyBillingStrategyService async reportUsage(params: z.infer) { const logger = await getLogger(); - logger.info( - { - name: 'billing.lemon-squeezy', - subscriptionItemId: params.subscriptionItemId, - }, - 'Reporting usage...', - ); + const ctx = { + name: this.namespace, + subscriptionItemId: params.subscriptionItemId, + }; + + logger.info(ctx, 'Reporting usage...'); const { error } = await createUsageRecord({ quantity: params.usage.quantity, @@ -230,8 +197,7 @@ export class LemonSqueezyBillingStrategyService if (error) { logger.error( { - name: 'billing.lemon-squeezy', - subscriptionItemId: params.subscriptionItemId, + ...ctx, error, }, 'Failed to report usage', @@ -240,13 +206,7 @@ export class LemonSqueezyBillingStrategyService throw new Error('Failed to report usage'); } - logger.info( - { - name: 'billing.lemon-squeezy', - subscriptionItemId: params.subscriptionItemId, - }, - 'Usage reported successfully', - ); + logger.info(ctx, 'Usage reported successfully'); return { success: true }; } @@ -257,7 +217,7 @@ export class LemonSqueezyBillingStrategyService const logger = await getLogger(); const ctx = { - name: 'billing.lemon-squeezy', + name: this.namespace, ...params, }; @@ -276,11 +236,58 @@ export class LemonSqueezyBillingStrategyService 'Failed to update subscription', ); - throw error; + throw new Error('Failed to update subscription'); } logger.info(ctx, 'Subscription updated successfully'); return { success: true }; } + + async getPlanById(planId: string) { + const logger = await getLogger(); + + const ctx = { + name: this.namespace, + planId, + }; + + logger.info(ctx, 'Retrieving plan by ID...'); + + const { error, data } = await getVariant(planId); + + if (error) { + logger.error( + { + ...ctx, + error, + }, + 'Failed to retrieve plan by ID', + ); + + throw new Error('Failed to retrieve plan by ID'); + } + + if (!data) { + logger.error( + { + ...ctx, + }, + 'Plan not found', + ); + + throw new Error('Plan not found'); + } + + logger.info(ctx, 'Plan retrieved successfully'); + + const attrs = data.data.attributes; + + return { + id: data.data.id, + name: attrs.name, + interval: attrs.interval ?? '', + amount: attrs.price, + }; + } } diff --git a/packages/billing/stripe/src/services/stripe-billing-strategy.service.ts b/packages/billing/stripe/src/services/stripe-billing-strategy.service.ts index 7c2f18f84..1a498476c 100644 --- a/packages/billing/stripe/src/services/stripe-billing-strategy.service.ts +++ b/packages/billing/stripe/src/services/stripe-billing-strategy.service.ts @@ -218,6 +218,36 @@ export class StripeBillingStrategyService } } + 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'); + } + } + private async stripeProvider(): Promise { return createStripeClient(); }