-
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
f882506
5406b9d
6d8fedc
57907b0
39f3a7d
eff7efb
7efb968
af3d39e
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,77 @@ | ||
| import axios from 'axios'; | ||
| import Logger from '../Logger'; | ||
| import { BadRequestError } from '../errors/Errors'; | ||
| import config from '../config'; | ||
|
|
||
| export enum KlaviyoEvent { | ||
| SubscriptionCancelled = 'Subscription Cancelled', | ||
| } | ||
|
|
||
| interface KlaviyoEventOptions { | ||
| email: string; | ||
| eventName: KlaviyoEvent; | ||
| } | ||
|
|
||
|
|
||
| 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() { | ||
| if (!config.KLAVIYO_API_KEY) { | ||
| throw new BadRequestError("Klaviyo API Key is required."); | ||
| } | ||
|
|
||
| this.apiKey = config.KLAVIYO_API_KEY; | ||
| this.baseUrl = config.KLAVIYO_BASE_URL || 'https://a.klaviyo.com/api'; | ||
| } | ||
|
|
||
| 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', | ||
| }, | ||
| }); | ||
|
|
||
| Logger.info(`[Klaviyo] ${eventName} tracked for ${email}`); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : 'Unknown error'; | ||
| Logger.error(`[Klaviyo] ${eventName} failed for ${email}: ${message}`); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async trackSubscriptionCancelled(email: string): Promise<void> { | ||
| await this.trackEvent({ | ||
| email, | ||
| eventName: KlaviyoEvent.SubscriptionCancelled, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| export const klaviyoService = new KlaviyoTrackingService(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import axios from 'axios'; | ||
| import { KlaviyoTrackingService, KlaviyoEvent } from '../../../src/services/klaviyo.service'; | ||
| import Logger from '../../../src/Logger'; | ||
| import { BadRequestError } from '../../../src/errors/Errors'; | ||
| import config from '../../../src/config'; | ||
|
|
||
| jest.mock('axios'); | ||
| jest.mock('../../../src/Logger'); | ||
|
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. I think the logger can be spied: |
||
| jest.mock('../../../src/config', () => ({ | ||
| __esModule: true, | ||
| default: { | ||
| KLAVIYO_API_KEY: 'pk_test_12345', | ||
| KLAVIYO_BASE_URL: 'https://a.klaviyo.com/api', | ||
| }, | ||
| })); | ||
|
|
||
| const mockedAxios = axios as jest.Mocked<typeof axios>; | ||
| const mockedLogger = Logger as jest.Mocked<typeof Logger>; | ||
|
|
||
|
|
||
| describe('KlaviyoTrackingService', () => { | ||
| let service: KlaviyoTrackingService; | ||
| const mockApiKey = 'pk_test_12345'; | ||
| const mockBaseUrl = 'https://a.klaviyo.com/api'; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| (config as any).KLAVIYO_API_KEY = mockApiKey; | ||
| (config as any).KLAVIYO_BASE_URL = mockBaseUrl; | ||
| service = new KlaviyoTrackingService(); | ||
|
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 instantiate the service in the service-factory. |
||
| }); | ||
|
|
||
| describe('Initialization', () => { | ||
| test('When instantiated without an API Key in config, then it throws an error', () => { | ||
| (config as any).KLAVIYO_API_KEY = undefined; | ||
| expect(() => new KlaviyoTrackingService()).toThrow(BadRequestError); | ||
| }); | ||
|
|
||
| test('When instantiated with valid config, then it initializes correctly', () => { | ||
| expect(() => new KlaviyoTrackingService()).not.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Tracking Subscription Cancelled', () => { | ||
| test('When tracking a cancellation, then it sends the correct payload to Klaviyo', async () => { | ||
| const email = 'user@example.com'; | ||
| const expectedUrl = `${mockBaseUrl}/events/`; | ||
|
|
||
| const expectedPayload = { | ||
| data: { | ||
| type: 'event', | ||
| attributes: { | ||
| profile: { | ||
| data: { | ||
| type: 'profile', | ||
| attributes: { email }, | ||
| }, | ||
| }, | ||
| metric: { | ||
| data: { | ||
| type: 'metric', | ||
| attributes: { name: KlaviyoEvent.SubscriptionCancelled }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const expectedHeaders = { | ||
| headers: { | ||
| Authorization: `Klaviyo-API-Key ${mockApiKey}`, | ||
| 'Content-Type': 'application/json', | ||
| revision: '2024-10-15', | ||
| }, | ||
| }; | ||
|
|
||
| mockedAxios.post.mockResolvedValue({ data: { status: 'ok' } }); | ||
|
|
||
| await service.trackSubscriptionCancelled(email); | ||
|
|
||
| expect(mockedAxios.post).toHaveBeenCalledTimes(1); | ||
| expect(mockedAxios.post).toHaveBeenCalledWith(expectedUrl, expectedPayload, expectedHeaders); | ||
| expect(mockedLogger.info).toHaveBeenCalledWith( | ||
| expect.stringContaining(`[Klaviyo] ${KlaviyoEvent.SubscriptionCancelled} tracked for ${email}`) | ||
| ); | ||
| }); | ||
|
|
||
| test('When axios fails, then the error is logged and re-thrown', async () => { | ||
| const email = 'error@example.com'; | ||
| const errorMessage = 'Network Error'; | ||
| const error = new Error(errorMessage); | ||
|
|
||
| mockedAxios.post.mockRejectedValue(error); | ||
|
|
||
| await expect(service.trackSubscriptionCancelled(email)).rejects.toThrow(errorMessage); | ||
|
|
||
| expect(mockedLogger.error).toHaveBeenCalledWith( | ||
| expect.stringContaining(`[Klaviyo] ${KlaviyoEvent.SubscriptionCancelled} failed for ${email}: ${errorMessage}`) | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
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).