Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/services/klaviyo.service.ts
Copy link
Collaborator

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).

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;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use throw new BadRequestError('...') instead.

}

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}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can extract this to an enum so you can do:
TRACK_ENENTS.SubscriptionCancelled or smth like that.

});
}
}
9 changes: 8 additions & 1 deletion src/webhooks/handleSubscriptionCanceled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
}
Expand Down
Loading