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
85 changes: 64 additions & 21 deletions src/app/analytics/impact.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const promoCode = {
amountOff: undefined,
codeId: 'promo_123',
percentOff: 99,
codeName: 'PROMO',
codeName: 'CNINTERNXTL',
};

const product = {
Expand Down Expand Up @@ -106,7 +106,7 @@ beforeEach(() => {

describe('Testing Impact Service', () => {
describe('savePaymentDataInLocalStorage', () => {
it('should save the correct amount to localStorage after applying coupon', () => {
it('When a coupon is applied, then it saves the discounted amount to localStorage', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

savePaymentDataInLocalStorage({
Expand All @@ -121,7 +121,7 @@ describe('Testing Impact Service', () => {
expect(setToLocalStorageSpy).toHaveBeenCalledWith('amountPaid', expectedAmount);
});

it('should save subscription ID when plan is not lifetime', () => {
it('When the plan is not lifetime, then it saves the subscription ID to localStorage', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

savePaymentDataInLocalStorage({
Expand All @@ -136,7 +136,7 @@ describe('Testing Impact Service', () => {
expect(setToLocalStorageSpy).toHaveBeenCalledWith('subscriptionId', subId);
});

it('should save payment intent ID when plan is lifetime', () => {
it('When the plan is lifetime, then it saves the payment intent ID to localStorage', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');
const lifetimeProduct = {
...product,
Expand All @@ -155,7 +155,7 @@ describe('Testing Impact Service', () => {
expect(setToLocalStorageSpy).toHaveBeenCalledWith('paymentIntentId', paymentIntentId);
});

it('should save product metadata including name, price ID, and currency', () => {
it('When saving payment data, then it saves the product name, price ID, and currency to localStorage', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

savePaymentDataInLocalStorage({
Expand All @@ -172,7 +172,7 @@ describe('Testing Impact Service', () => {
expect(setToLocalStorageSpy).toHaveBeenCalledWith('currency', product.price.currency);
});

it('should save coupon code when provided', () => {
it('When a coupon code is provided, then it saves the coupon code to localStorage', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

savePaymentDataInLocalStorage({
Expand All @@ -187,7 +187,7 @@ describe('Testing Impact Service', () => {
expect(setToLocalStorageSpy).toHaveBeenCalledWith('couponCode', promoCode.codeName);
});

it('should save isFirstPurchase flag to localStorage', () => {
it('When saving payment data, then it saves the isFirstPurchase flag to localStorage', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

savePaymentDataInLocalStorage({
Expand All @@ -205,15 +205,15 @@ describe('Testing Impact Service', () => {

describe('trackSignUp', () => {
describe('gtag tracking', () => {
it('should send User Signup event to gtag', async () => {
it('When trackSignUp is called, then it sends a User Signup event to gtag', async () => {
const gTagSpy = vi.spyOn(globalThis.window, 'gtag');

await trackSignUp(mockedUserUuid);

expect(gTagSpy).toHaveBeenCalledWith('event', 'User Signup');
});

it('should report error when gtag fails but continue execution', async () => {
it('When gtag throws an error, then it reports the error and continues execution', async () => {
const unknownError = new Error('gtag Error');
const gTagSpy = vi.spyOn(globalThis.window, 'gtag').mockImplementation(() => {
throw unknownError;
Expand All @@ -228,7 +228,7 @@ describe('Testing Impact Service', () => {
});

describe('Impact API tracking', () => {
it('should send signup event to Impact API with correct payload', async () => {
it('When trackSignUp is called, then it sends a signup event to the Impact API with the correct payload', async () => {
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});

await trackSignUp(mockedUserUuid);
Expand All @@ -246,7 +246,7 @@ describe('Testing Impact Service', () => {
);
});

it('should include message ID in Impact API payload', async () => {
it('When trackSignUp is called, then it includes the message ID in the Impact API payload', async () => {
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});

await trackSignUp(mockedUserUuid);
Expand All @@ -256,7 +256,7 @@ describe('Testing Impact Service', () => {
expect(callArgs.messageId).toBe(mockedUserUuid);
});

it('should not send to Impact API when source is direct', async () => {
it('When the source is direct, then it does not send to the Impact API', async () => {
const getCookieMock = await import('./utils');
vi.mocked(getCookieMock.getCookie).mockImplementation((key) => {
if (key === 'impactSource') return 'direct';
Expand All @@ -274,7 +274,7 @@ describe('Testing Impact Service', () => {

describe('trackPaymentConversion', () => {
describe('Impact API tracking', () => {
it('should send payment conversion to Impact API with correct data', async () => {
it('When trackPaymentConversion is called, then it sends the conversion to the Impact API with the correct data', async () => {
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});

await trackPaymentConversion();
Expand All @@ -298,7 +298,7 @@ describe('Testing Impact Service', () => {
);
});

it('should use minimum value of 0.01 when amount is 0 (free purchase)', async () => {
it('When the amount paid is 0, then it uses 0.01 as the minimum impact value', async () => {
vi.spyOn(localStorageService, 'get').mockImplementation((key) => {
if (key === 'amountPaid') return '0';
if (key === 'subscriptionId') return subId;
Expand All @@ -314,7 +314,7 @@ describe('Testing Impact Service', () => {
expect(callArgs.properties.impact_value).toBe(0.01);
});

it('should include coupon code in properties when available', async () => {
it('When a coupon code is available, then it includes it in the Impact API properties', async () => {
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});

await trackPaymentConversion();
Expand All @@ -323,7 +323,7 @@ describe('Testing Impact Service', () => {
expect(callArgs.properties).toHaveProperty('order_promo_code', promoCode.codeName);
});

it('should report error when Impact API call fails', async () => {
it('When the Impact API call fails, then it reports the error', async () => {
const unknownError = new Error('API Error');
const axiosSpy = vi.spyOn(axios, 'post').mockRejectedValue(unknownError);
const errorServiceSpy = vi.spyOn(errorService, 'reportError');
Expand All @@ -334,7 +334,7 @@ describe('Testing Impact Service', () => {
expect(errorServiceSpy).toHaveBeenCalledWith(unknownError);
});

it('should not send to Impact when source is direct and no coupon code', async () => {
it('When the source is direct and no coupon code is present, then it does not send to Impact', async () => {
const getCookieMock = await import('./utils');
vi.mocked(getCookieMock.getCookie).mockImplementation((key) => {
if (key === 'impactSource') return 'direct';
Expand All @@ -353,7 +353,50 @@ describe('Testing Impact Service', () => {
expect(axiosSpy).not.toHaveBeenCalled();
});

it('should not send to Impact when isFirstPurchase is false', async () => {
it('When the source is direct and the coupon code is not whitelisted, then it does not send to Impact', async () => {
const getCookieMock = await import('./utils');
vi.mocked(getCookieMock.getCookie).mockImplementation((key) => {
if (key === 'impactSource') return 'direct';
return '';
});
vi.spyOn(localStorageService, 'get').mockImplementation((key) => {
if (key === 'couponCode') return 'NOT_WHITELISTED';
if (key === 'amountPaid') return expectedAmount;
if (key === 'isFirstPurchase') return 'true';
return null;
});
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});

await trackPaymentConversion();

expect(axiosSpy).not.toHaveBeenCalled();
});

it('When the source is direct but the coupon code is whitelisted, then it sends to Impact', async () => {
const getCookieMock = await import('./utils');
vi.mocked(getCookieMock.getCookie).mockImplementation((key) => {
if (key === 'impactSource') return 'direct';
if (key === 'impactAnonymousId') return ''; // Empty, so it uses uuidV4
return '';
});
vi.spyOn(localStorageService, 'get').mockImplementation((key) => {
if (key === 'couponCode') return 'CNINTERNXT'; // In whitelist
if (key === 'amountPaid') return expectedAmount;
if (key === 'subscriptionId') return subId;
if (key === 'isFirstPurchase') return 'true';
return null;
});
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});

await trackPaymentConversion();

expect(axiosSpy).toHaveBeenCalledTimes(1);
const callArgs = axiosSpy.mock.calls[0][1] as { properties: Record<string, unknown>; anonymousId: string };
expect(callArgs.properties).toHaveProperty('order_promo_code', 'CNINTERNXT');
expect(callArgs.anonymousId).toBe(mockedUserUuid); // Fallback to uuidV4
});

it('When isFirstPurchase is false, then it does not send to Impact', async () => {
vi.spyOn(localStorageService, 'get').mockImplementation((key) => {
if (key === 'isFirstPurchase') return 'false';
if (key === 'amountPaid') return expectedAmount;
Expand All @@ -369,7 +412,7 @@ describe('Testing Impact Service', () => {
});

describe('Error handling', () => {
it('should handle missing user settings gracefully', async () => {
it('When user settings are missing, then it resolves without throwing', async () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.spyOn(localStorageService, 'getUser').mockReturnValue(null);

Expand All @@ -378,13 +421,13 @@ describe('Testing Impact Service', () => {
consoleWarnSpy.mockRestore();
});

it('should continue execution when gtag is not available', async () => {
it('When gtag is not available, then it continues execution without throwing', async () => {
globalThis.window.gtag = undefined as any;

await expect(trackPaymentConversion()).resolves.not.toThrow();
});

it('should handle errors in entire function gracefully', async () => {
it('When an unexpected error occurs, then it resolves without throwing', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(localStorageService, 'getUser').mockImplementation(() => {
throw new Error('Storage Error');
Expand Down
7 changes: 5 additions & 2 deletions src/app/analytics/impact.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,13 @@ export async function trackPaymentConversion(): Promise<void> {
}

const IMPACT_API = envService.getVariable('impactApiUrl');
const anonymousID = getCookie('impactAnonymousId');
const anonymousID = getCookie('impactAnonymousId') || uuidV4();
const source = getCookie('impactSource');

if (isFirstPurchase && ((source && source !== 'direct') || couponCode)) {
const IMPACT_COUPON_WHITELIST = ['CNINTERNXT', 'CNINTERNXTL'];
const isImpactCoupon = couponCode && IMPACT_COUPON_WHITELIST.includes(couponCode.toUpperCase());

if (isFirstPurchase && ((source && source !== 'direct') || isImpactCoupon)) {
try {
await axios.post(IMPACT_API, {
anonymousId: anonymousID,
Expand Down
Loading