From f8825064f5ca5180abf4d04dae7851afc9dc634c Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Wed, 11 Feb 2026 14:26:24 +0100 Subject: [PATCH 1/2] feat: implement klaviyo tracking services --- src/services/klaviyo.service.ts | 70 ++++++++++++++++++++++ src/webhooks/handleSubscriptionCanceled.ts | 9 ++- 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/services/klaviyo.service.ts 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..6d8624ed 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('pk_9c5b5074b318bb02fc7f575102379c25b1'); + 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; } From 5406b9de4c4bce28216cf8e5f42124bfb94ae976 Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Wed, 11 Feb 2026 14:58:00 +0100 Subject: [PATCH 2/2] Update handleSubscriptionCanceled.ts --- src/webhooks/handleSubscriptionCanceled.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webhooks/handleSubscriptionCanceled.ts b/src/webhooks/handleSubscriptionCanceled.ts index 6d8624ed..e75a3ee6 100644 --- a/src/webhooks/handleSubscriptionCanceled.ts +++ b/src/webhooks/handleSubscriptionCanceled.ts @@ -61,7 +61,7 @@ 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('pk_9c5b5074b318bb02fc7f575102379c25b1'); + const klaviyoService = new KlaviyoTrackingService(process.env.KLAVIYO_API_KEY); if (isObjectStorageProduct(productMetadata)) { await handleObjectStorageSubscriptionCancelled(customer, subscription, objectStorageService, paymentService, log);