-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
f882506
feat: implement klaviyo tracking services
jaaaaavier 5406b9d
Update handleSubscriptionCanceled.ts
jaaaaavier 6d8fedc
feat: review changes
jaaaaavier 57907b0
Create klaviyo.service.test.ts
jaaaaavier 39f3a7d
Update sonarcloud-analysis.yml
jaaaaavier eff7efb
Update sonarcloud-analysis.yml
jaaaaavier 7efb968
Update tests.yaml
jaaaaavier af3d39e
feat: update after revision
jaaaaavier e6c5dd6
add properties
jaaaaavier d805f23
Update config.ts
jaaaaavier cf1eacc
feat: add new env variables to base required varaibles
jaaaaavier f406087
Update config.ts
jaaaaavier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
jaaaaavier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
| } | ||
|
|
||
| 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(); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| 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'; | ||
| import { createTestServices } from '../helpers/services-factory'; | ||
|
|
||
| jest.mock('axios'); | ||
| jest.mock('../../../src/config', () => ({ | ||
| __esModule: true, | ||
| default: { | ||
| KLAVIYO_API_KEY: 'pk_test_12345', | ||
| KLAVIYO_BASE_URL: 'https://a.klaviyo.com/api', | ||
| STRIPE_SECRET_KEY: 'sk_test_12345', | ||
| }, | ||
| })); | ||
|
|
||
| const mockedAxios = axios as jest.Mocked<typeof axios>; | ||
|
|
||
|
|
||
| describe('KlaviyoTrackingService', () => { | ||
| let service: KlaviyoTrackingService; | ||
| let loggerInfoSpy: jest.SpyInstance; | ||
| let loggerErrorSpy: jest.SpyInstance; | ||
|
|
||
| const mockApiKey = 'pk_test_12345'; | ||
| const mockBaseUrl = 'https://a.klaviyo.com/api'; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| loggerInfoSpy = jest.spyOn(Logger, 'info').mockImplementation(); | ||
| loggerErrorSpy = jest.spyOn(Logger, 'error').mockImplementation(); | ||
|
|
||
| (config as any).KLAVIYO_API_KEY = mockApiKey; | ||
| (config as any).KLAVIYO_BASE_URL = mockBaseUrl; | ||
| service = createTestServices({ stripe: {} as any }).klaviyoTrackingService; | ||
| }); | ||
|
|
||
| describe('Initialization', () => { | ||
| test('When instantiated without an API Key in config, then it throws an error', () => { | ||
| (config as any).KLAVIYO_API_KEY = undefined; | ||
| expect(() => createTestServices({ stripe: {} as any }).klaviyoTrackingService).toThrow(BadRequestError); | ||
| }); | ||
|
|
||
| test('When instantiated with valid config, then it initializes correctly', () => { | ||
| expect(() => createTestServices({ stripe: {} as any }).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(loggerInfoSpy).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(loggerErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining(`[Klaviyo] ${KlaviyoEvent.SubscriptionCancelled} failed for ${email}: ${errorMessage}`) | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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).