diff --git a/src/services/klaviyo.service.ts b/src/services/klaviyo.service.ts new file mode 100644 index 00000000..3d19af0f --- /dev/null +++ b/src/services/klaviyo.service.ts @@ -0,0 +1,70 @@ +import axios from 'axios'; + +interface KlaviyoEventOptions { + email: string; + eventName: string; +} + +export class KlaviyoTrackingService { + private readonly apiKey: string; + private readonly baseUrl: string; + + constructor( + apiKey: string | undefined = process.env.KLAVIYO_API_KEY, + baseUrl: string | undefined = process.env.KLAVIYO_BASE_URL + ) { + if (!apiKey) { + throw new Error("Klaviyo API Key is required."); + } + + this.apiKey = apiKey; + this.baseUrl = baseUrl ?? ""; + } + + private async trackEvent(options: KlaviyoEventOptions): Promise { + const { email, eventName } = options; + + const payload = { + data: { + type: 'event', + attributes: { + profile: { + data: { + type: 'profile', + attributes: { email }, + }, + }, + metric: { + data: { + type: 'metric', + attributes: { name: eventName }, + }, + }, + }, + }, + }; + + try { + await axios.post(`${this.baseUrl}/events/`, payload, { + headers: { + Authorization: `Klaviyo-API-Key ${this.apiKey}`, + 'Content-Type': 'application/json', + revision: '2024-10-15', + }, + }); + + console.log(`[Klaviyo] ${eventName} tracked for ${email}`); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(`[Klaviyo] ${eventName} failed for ${email}:`, message); + throw error; + } + } + + async trackSubscriptionCancelled(email: string): Promise { + await this.trackEvent({ + email, + eventName: 'Subscription Cancelled', + }); + } +} \ No newline at end of file diff --git a/src/webhooks/handleSubscriptionCanceled.ts b/src/webhooks/handleSubscriptionCanceled.ts index 56a27986..e75a3ee6 100644 --- a/src/webhooks/handleSubscriptionCanceled.ts +++ b/src/webhooks/handleSubscriptionCanceled.ts @@ -12,6 +12,7 @@ import { TierNotFoundError, TiersService } from '../services/tiers.service'; import { Service } from '../core/users/Tier'; import { stripePaymentsAdapter } from '../infrastructure/adapters/stripe.adapter'; import { Customer } from '../infrastructure/domain/entities/customer'; +import { KlaviyoTrackingService } from '../services/klaviyo.service'; function isObjectStorageProduct(meta: Stripe.Metadata): boolean { return !!meta && !!meta.type && meta.type === 'object-storage'; @@ -60,7 +61,8 @@ export default async function handleSubscriptionCanceled( const productId = subscription.items.data[0].price.product as string; const { metadata: productMetadata } = await paymentService.getProduct(productId); const customer = await stripePaymentsAdapter.getCustomer(customerId); - + const klaviyoService = new KlaviyoTrackingService(process.env.KLAVIYO_API_KEY); + if (isObjectStorageProduct(productMetadata)) { await handleObjectStorageSubscriptionCancelled(customer, subscription, objectStorageService, paymentService, log); return; @@ -101,6 +103,11 @@ export default async function handleSubscriptionCanceled( } catch (error) { const err = error as Error; log.error(`[SUB CANCEL/ERROR]: Error canceling tier product. ERROR: ${err.stack ?? err.message}`); + try { + await klaviyoService.trackSubscriptionCancelled(customer.email); + } catch (error) { + log.error(`[KLAVIYO] Failed to track cancellation for ${customerId}`); + } if (!(error instanceof TierNotFoundError)) { throw error; }