-
Notifications
You must be signed in to change notification settings - Fork 4
[MKT-748]:feat/implement tracking services for cancelled subscriptions #351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import axios from 'axios'; | ||
|
|
||
| interface KlaviyoEventOptions { | ||
| email: string; | ||
| eventName: string; | ||
| } | ||
|
|
||
| export class KlaviyoTrackingService { | ||
| private readonly apiKey: string; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can assign here directly the variables if you want, no need to pass it as props. Also, use config file instead of process.env. |
||
| 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."); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| } | ||
|
|
||
| this.apiKey = apiKey; | ||
| this.baseUrl = baseUrl ?? ""; | ||
| } | ||
|
|
||
| private async trackEvent(options: KlaviyoEventOptions): Promise<void> { | ||
| 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}`); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Logger instead |
||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : 'Unknown error'; | ||
| console.error(`[Klaviyo] ${eventName} failed for ${email}:`, message); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Logger instead |
||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async trackSubscriptionCancelled(email: string): Promise<void> { | ||
| await this.trackEvent({ | ||
| email, | ||
| eventName: 'Subscription Cancelled', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: you can extract this to an enum so you can do: |
||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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}`); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log the error here also, and it would be nice to use Logger we can remove log in the future. |
||
| } | ||
| if (!(error instanceof TierNotFoundError)) { | ||
| throw error; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Export the class here directly, e.g.:
const klaviyoService = new KlaviyoService();so you can export it directly without instantiating it every time (you can use the api key directly).