Add handling for paid invoices in billing services

In response to paid invoices, a new callback method has been introduced in the billing services. This feature includes changes in billing-webhook-handler.service.ts, lemon-squeezy-webhook-handler.service.ts, and other related files. The new method fetches and handles paid invoice information from Stripe and LemonSqueezy subscriptions.
This commit is contained in:
gbuomprisco
2024-06-20 00:02:25 +08:00
parent 73c721557f
commit 95fa8fb7c6
5 changed files with 205 additions and 5 deletions

View File

@@ -1,4 +1,8 @@
import { getOrder, getVariant } from '@lemonsqueezy/lemonsqueezy.js';
import {
getOrder,
getSubscription,
getVariant,
} from '@lemonsqueezy/lemonsqueezy.js';
import { BillingConfig, BillingWebhookHandlerService } from '@kit/billing';
import { getLogger } from '@kit/shared/logger';
@@ -6,6 +10,7 @@ import { Database } from '@kit/supabase/database';
import { getLemonSqueezyEnv } from '../schema/lemon-squeezy-server-env.schema';
import { OrderWebhook } from '../types/order-webhook';
import { SubscriptionInvoiceWebhook } from '../types/subscription-invoice-webhook';
import { SubscriptionWebhook } from '../types/subscription-webhook';
import { initializeLemonSqueezyClient } from './lemon-squeezy-sdk';
import { createLemonSqueezySubscriptionPayloadBuilderService } from './lemon-squeezy-subscription-payload-builder.service';
@@ -89,7 +94,7 @@ export class LemonSqueezyWebhookHandlerService
}
async handleWebhookEvent(
event: OrderWebhook | SubscriptionWebhook,
event: OrderWebhook | SubscriptionWebhook | SubscriptionInvoiceWebhook,
params: {
onCheckoutSessionCompleted: (
data: UpsertSubscriptionParams | UpsertOrderParams,
@@ -100,7 +105,10 @@ export class LemonSqueezyWebhookHandlerService
onSubscriptionDeleted: (subscriptionId: string) => Promise<unknown>;
onPaymentSucceeded: (sessionId: string) => Promise<unknown>;
onPaymentFailed: (sessionId: string) => Promise<unknown>;
onEvent?: (event: OrderWebhook | SubscriptionWebhook) => Promise<unknown>;
onInvoicePaid: (
data: UpsertSubscriptionParams | UpsertOrderParams,
) => Promise<unknown>;
onEvent?: (event: unknown) => Promise<unknown>;
},
) {
const eventName = event.meta.event_name;
@@ -134,6 +142,13 @@ export class LemonSqueezyWebhookHandlerService
);
}
case 'subscription_payment_success': {
return this.handleInvoicePaid(
event as SubscriptionInvoiceWebhook,
params.onInvoicePaid,
);
}
default: {
if (params.onEvent) {
return params.onEvent(event);
@@ -298,6 +313,80 @@ export class LemonSqueezyWebhookHandlerService
return onSubscriptionDeletedCallback(subscription.data.id);
}
private async handleInvoicePaid(
event: SubscriptionInvoiceWebhook,
onInvoicePaidCallback: (
subscription: UpsertSubscriptionParams,
) => Promise<unknown>,
) {
await initializeLemonSqueezyClient();
const attrs = event.data.attributes;
const subscriptionId = event.data.id;
const accountId = event.meta.custom_data.account_id;
const customerId = attrs.customer_id.toString();
const status = attrs.status;
const createdAt = attrs.created_at;
const { data: subscriptionResponse } =
await getSubscription(subscriptionId);
const subscription = subscriptionResponse?.data.attributes;
if (!subscription) {
const logger = await getLogger();
logger.error(
{
subscriptionId,
accountId,
name: this.namespace,
},
'Failed to fetch subscription',
);
return;
}
const variantId = subscription.variant_id;
const productId = subscription.product_id;
const endsAt = subscription.ends_at;
const renewsAt = subscription.renews_at;
const trialEndsAt = subscription.trial_ends_at;
const intervalCount = subscription.billing_anchor;
const interval = intervalCount === 1 ? 'month' : 'year';
const payloadBuilderService =
createLemonSqueezySubscriptionPayloadBuilderService();
const lineItems = [
{
id: subscription.order_item_id.toString(),
product: productId.toString(),
variant: variantId.toString(),
quantity: subscription.first_subscription_item?.quantity ?? 1,
priceAmount: attrs.total,
},
];
const payload = payloadBuilderService.withBillingConfig(this.config).build({
customerId,
id: subscriptionId,
accountId,
lineItems,
status,
interval,
intervalCount,
currency: attrs.currency,
periodStartsAt: new Date(createdAt).getTime(),
periodEndsAt: new Date(renewsAt ?? endsAt).getTime(),
cancelAtPeriodEnd: subscription.cancelled,
trialStartsAt: trialEndsAt ? new Date(createdAt).getTime() : null,
trialEndsAt: trialEndsAt ? new Date(trialEndsAt).getTime() : null,
});
return onInvoicePaidCallback(payload);
}
private getOrderStatus(status: OrderStatus) {
switch (status) {
case 'paid':

View File

@@ -0,0 +1,53 @@
export interface SubscriptionInvoiceWebhook {
meta: Meta;
data: Data;
}
interface Data {
type: string;
id: string;
attributes: Attributes;
}
interface Meta {
event_name: string;
custom_data: {
account_id: string;
};
}
interface Attributes {
store_id: number;
subscription_id: number;
customer_id: number;
user_name: string;
user_email: string;
billing_reason: string;
card_brand: string;
card_last_four: string;
currency: string;
currency_rate: string;
status: string;
status_formatted: string;
refunded: boolean;
refunded_at: string | null;
subtotal: number;
discount_total: number;
tax: number;
tax_inclusive: boolean;
total: number;
subtotal_usd: number;
discount_total_usd: number;
tax_usd: number;
total_usd: number;
subtotal_formatted: string;
discount_total_formatted: string;
tax_formatted: string;
total_formatted: string;
urls: {
invoice_url: string;
};
created_at: string;
updated_at: string;
test_mode: boolean;
}