Add Lemon Squeezy Billing System

This commit is contained in:
giancarlo
2024-04-01 21:43:18 +08:00
parent 84a4b45bcd
commit 8784a40a69
59 changed files with 424 additions and 74 deletions

View File

@@ -0,0 +1,195 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { BillingWebhookHandlerService } from '@kit/billing';
import { Logger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database';
export class BillingEventHandlerService {
private readonly namespace = 'billing';
constructor(
private readonly clientProvider: () => SupabaseClient<Database>,
private readonly strategy: BillingWebhookHandlerService,
) {}
async handleWebhookEvent(request: Request) {
const event = await this.strategy.verifyWebhookSignature(request);
if (!event) {
throw new Error('Invalid signature');
}
return this.strategy.handleWebhookEvent(event, {
onSubscriptionDeleted: async (subscriptionId: string) => {
const client = this.clientProvider();
// Handle the subscription deleted event
// here we delete the subscription from the database
Logger.info(
{
namespace: this.namespace,
subscriptionId,
},
'Processing subscription deleted event',
);
const { error } = await client
.from('subscriptions')
.delete()
.match({ id: subscriptionId });
if (error) {
throw new Error('Failed to delete subscription');
}
Logger.info(
{
namespace: this.namespace,
subscriptionId,
},
'Successfully deleted subscription',
);
},
onSubscriptionUpdated: async (subscription) => {
const client = this.clientProvider();
const ctx = {
namespace: this.namespace,
subscriptionId: subscription.target_subscription_id,
provider: subscription.billing_provider,
accountId: subscription.target_account_id,
customerId: subscription.target_customer_id,
};
Logger.info(ctx, 'Processing subscription updated event');
// Handle the subscription updated event
// here we update the subscription in the database
const { error } = await client.rpc('upsert_subscription', subscription);
if (error) {
Logger.error(
{
error,
...ctx,
},
'Failed to update subscription',
);
throw new Error('Failed to update subscription');
}
Logger.info(ctx, 'Successfully updated subscription');
},
onCheckoutSessionCompleted: async (payload) => {
// Handle the checkout session completed event
// here we add the subscription to the database
const client = this.clientProvider();
// Check if the payload contains an order_id
// if it does, we add an order, otherwise we add a subscription
if ('target_order_id' in payload) {
const ctx = {
namespace: this.namespace,
orderId: payload.target_order_id,
provider: payload.billing_provider,
accountId: payload.target_account_id,
customerId: payload.target_customer_id,
};
Logger.info(ctx, 'Processing order completed event...');
const { error } = await client.rpc('upsert_order', payload);
if (error) {
Logger.error({ ...ctx, error }, 'Failed to add order');
throw new Error('Failed to add order');
}
Logger.info(ctx, 'Successfully added order');
} else {
const ctx = {
namespace: this.namespace,
subscriptionId: payload.target_subscription_id,
provider: payload.billing_provider,
accountId: payload.target_account_id,
customerId: payload.target_customer_id,
};
Logger.info(ctx, 'Processing checkout session completed event...');
const { error } = await client.rpc('upsert_subscription', payload);
if (error) {
Logger.error({ ...ctx, error }, 'Failed to add subscription');
throw new Error('Failed to add subscription');
}
Logger.info(ctx, 'Successfully added subscription');
}
},
onPaymentSucceeded: async (sessionId: string) => {
const client = this.clientProvider();
// Handle the payment succeeded event
// here we update the payment status in the database
Logger.info(
{
namespace: this.namespace,
sessionId,
},
'Processing payment succeeded event',
);
const { error } = await client
.from('orders')
.update({ status: 'succeeded' })
.match({ session_id: sessionId });
if (error) {
throw new Error('Failed to update payment status');
}
Logger.info(
{
namespace: 'billing',
sessionId,
},
'Successfully updated payment status',
);
},
onPaymentFailed: async (sessionId: string) => {
const client = this.clientProvider();
// Handle the payment failed event
// here we update the payment status in the database
Logger.info(
{
namespace: this.namespace,
sessionId,
},
'Processing payment failed event',
);
const { error } = await client
.from('orders')
.update({ status: 'failed' })
.match({ session_id: sessionId });
if (error) {
throw new Error('Failed to update payment status');
}
Logger.info(
{
namespace: this.namespace,
sessionId,
},
'Successfully updated payment status',
);
},
});
}
}

View File

@@ -0,0 +1,31 @@
import { z } from 'zod';
import {
BillingProviderSchema,
BillingWebhookHandlerService,
} from '@kit/billing';
export class BillingEventHandlerFactoryService {
static async GetProviderStrategy(
provider: z.infer<typeof BillingProviderSchema>,
): Promise<BillingWebhookHandlerService> {
switch (provider) {
case 'stripe': {
const { StripeWebhookHandlerService } = await import('@kit/stripe');
return new StripeWebhookHandlerService();
}
case 'paddle': {
throw new Error('Paddle is not supported yet');
}
case 'lemon-squeezy': {
throw new Error('Lemon Squeezy is not supported yet');
}
default:
throw new Error(`Unsupported billing provider: ${provider as string}`);
}
}
}

View File

@@ -0,0 +1,20 @@
import { Database } from '@kit/supabase/database';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { BillingEventHandlerService } from './billing-event-handler.service';
import { BillingEventHandlerFactoryService } from './billing-gateway-factory.service';
/**
* @description This function retrieves the billing provider from the database and returns a
* new instance of the `BillingGatewayService` class. This class is used to interact with the server actions
* defined in the host application.
*/
export async function getBillingEventHandlerService(
clientProvider: () => ReturnType<typeof getSupabaseServerActionClient>,
provider: Database['public']['Enums']['billing_provider'],
) {
const strategy =
await BillingEventHandlerFactoryService.GetProviderStrategy(provider);
return new BillingEventHandlerService(clientProvider, strategy);
}