Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
30 changes: 2 additions & 28 deletions src/app/analytics/ga.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ describe('Testing GA Service', () => {

vi.mocked(localStorageService.get).mockImplementation((key) => {
const store: Record<string, string> = {
subscriptionId: 'sub_12345',
paymentIntentId: '',
priceId: 'price_yearly_2tb',
currency: 'EUR',
Expand All @@ -236,7 +235,7 @@ describe('Testing GA Service', () => {
expect(event).toMatchObject({
event: 'purchase',
ecommerce: {
transaction_id: 'sub_12345',
transaction_id: 'user_uuid_123',
currency: 'EUR',
value: 95.9,
items: [
Expand All @@ -260,7 +259,6 @@ describe('Testing GA Service', () => {
vi.mocked(localStorageService.getUser).mockReturnValue({ uuid: 'user_uuid' } as any);
vi.mocked(localStorageService.get).mockImplementation((key) => {
if (key === 'paymentIntentId') return 'pi_999';
if (key === 'subscriptionId') return 'sub_888';
if (key === 'amountPaid') return '100';
if (key === 'itemOriginalPrice') return '119.88';
if (key === 'checkout_item_data')
Expand All @@ -279,34 +277,10 @@ describe('Testing GA Service', () => {
expect(event.ecommerce.transaction_id).toBe('pi_999');
});

it('should use subscription ID when payment intent is not available', () => {
vi.mocked(localStorageService.getUser).mockReturnValue({ uuid: 'user_uuid' } as any);
vi.mocked(localStorageService.get).mockImplementation((key) => {
if (key === 'paymentIntentId') return null;
if (key === 'subscriptionId') return 'sub_888';
if (key === 'amountPaid') return '100';
if (key === 'itemOriginalPrice') return '119.88';
if (key === 'checkout_item_data')
return JSON.stringify({
item_name: '2TB Year Plan',
item_category: 'Individual',
item_variant: 'year',
discount: 0,
});
return '';
});

gaService.trackPurchase();

const event = globalThis.window.dataLayer[0] as any;
expect(event.ecommerce.transaction_id).toBe('sub_888');
});

it('should fallback to user UUID when neither payment intent nor subscription ID are available', () => {
it('should fallback to user UUID when payment intent is not available', () => {
vi.mocked(localStorageService.getUser).mockReturnValue({ uuid: 'user_fallback_uuid' } as any);
vi.mocked(localStorageService.get).mockImplementation((key) => {
if (key === 'paymentIntentId') return null;
if (key === 'subscriptionId') return null;
if (key === 'amountPaid') return '100';
if (key === 'itemOriginalPrice') return '119.88';
if (key === 'checkout_item_data')
Expand Down
3 changes: 1 addition & 2 deletions src/app/analytics/ga.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ function trackPurchase(): void {
return;
}

const subscriptionId = localStorageService.get('subscriptionId');
const paymentIntentId = localStorageService.get('paymentIntentId');
const priceId = localStorageService.get('priceId');
const currency = localStorageService.get('currency');
Expand All @@ -173,7 +172,7 @@ function trackPurchase(): void {
console.error('[GA Service] Error parsing checkout_item_data:', parseError);
}

const transactionId = paymentIntentId || subscriptionId || uuid;
const transactionId = paymentIntentId || uuid;
const currencyCode = currency ?? 'EUR';

const itemName = checkoutItemData?.item_name || 'Unknown Plan';
Expand Down
93 changes: 44 additions & 49 deletions src/app/analytics/impact.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ vi.mock('services/error.service', () => ({
},
}));

const subId = 'sub_123';
const paymentIntentId = 'py_123';
const mockedUserUuid = '00000000-0000-0000-0000-0000000000';
const mockImpactApiUrl = 'mock-impact-api-url';
Expand Down Expand Up @@ -93,7 +92,6 @@ beforeEach(() => {

vi.spyOn(localStorageService, 'get').mockImplementation((key) => {
if (key === 'paymentIntentId') return paymentIntentId;
if (key === 'subscriptionId') return subId;
if (key === 'productName') return planName;
if (key === 'priceId') return product.price.id;
if (key === 'currency') return product.price.currency;
Expand All @@ -106,11 +104,10 @@ beforeEach(() => {

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

savePaymentDataInLocalStorage({
subscriptionId: subId,
paymentIntentId,
selectedPlan: product as PriceWithTax,
users: 1,
Expand All @@ -121,32 +118,12 @@ describe('Testing Impact Service', () => {
expect(setToLocalStorageSpy).toHaveBeenCalledWith('amountPaid', expectedAmount);
});

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

savePaymentDataInLocalStorage({
subscriptionId: subId,
paymentIntentId: undefined,
selectedPlan: product as PriceWithTax,
users: 1,
couponCodeData: promoCode,
isFirstPurchase: true,
});

expect(setToLocalStorageSpy).toHaveBeenCalledWith('subscriptionId', subId);
});

it('should save payment intent ID when plan is lifetime', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');
const lifetimeProduct = {
...product,
price: { ...product.price, interval: 'lifetime' },
};

savePaymentDataInLocalStorage({
subscriptionId: undefined,
paymentIntentId,
selectedPlan: lifetimeProduct as PriceWithTax,
selectedPlan: product as PriceWithTax,
users: 1,
couponCodeData: promoCode,
isFirstPurchase: true,
Expand All @@ -155,11 +132,10 @@ 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 product metadata including name, price ID, and currency', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

savePaymentDataInLocalStorage({
subscriptionId: subId,
paymentIntentId,
selectedPlan: product as PriceWithTax,
users: 1,
Expand All @@ -172,11 +148,10 @@ describe('Testing Impact Service', () => {
expect(setToLocalStorageSpy).toHaveBeenCalledWith('currency', product.price.currency);
});

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

savePaymentDataInLocalStorage({
subscriptionId: subId,
paymentIntentId,
selectedPlan: product as PriceWithTax,
users: 1,
Expand All @@ -187,11 +162,10 @@ 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 isFirstPurchase flag to localStorage', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

savePaymentDataInLocalStorage({
subscriptionId: subId,
paymentIntentId,
selectedPlan: product as PriceWithTax,
users: 1,
Expand All @@ -205,15 +179,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 User Signup event to gtag', async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Avoid using technical descriptions. Describe what it does, not how it does (calling methods and this things), Same for the other descriptions

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 fails, then it reports error but continues execution', async () => {
const unknownError = new Error('gtag Error');
const gTagSpy = vi.spyOn(globalThis.window, 'gtag').mockImplementation(() => {
throw unknownError;
Expand All @@ -228,7 +202,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 signup event to Impact API with correct payload', async () => {
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});

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

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

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

it('should not send to Impact API when source is direct', async () => {
it('When source is direct, then it does not send to Impact API', async () => {
const getCookieMock = await import('./utils');
vi.mocked(getCookieMock.getCookie).mockImplementation((key) => {
if (key === 'impactSource') return 'direct';
Expand All @@ -274,7 +248,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 payment conversion to Impact API with correct data', async () => {
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});

await trackPaymentConversion();
Expand All @@ -287,7 +261,6 @@ describe('Testing Impact Service', () => {
timestamp: expect.any(String),
properties: expect.objectContaining({
impact_value: parseFloat(expectedAmount),
subscription_id: subId,
payment_intent: paymentIntentId,
order_promo_code: promoCode.codeName,
}),
Expand All @@ -298,10 +271,9 @@ describe('Testing Impact Service', () => {
);
});

it('should use minimum value of 0.01 when amount is 0 (free purchase)', async () => {
it('When amount is 0 (free purchase), then it uses minimum value of 0.01', async () => {
vi.spyOn(localStorageService, 'get').mockImplementation((key) => {
if (key === 'amountPaid') return '0';
if (key === 'subscriptionId') return subId;
if (key === 'couponCode') return promoCode.codeName;
if (key === 'isFirstPurchase') return 'true';
return null;
Expand All @@ -314,7 +286,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 coupon code is available, then it includes coupon code in properties', async () => {
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});

await trackPaymentConversion();
Expand All @@ -323,7 +295,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 Impact API call fails, then it reports 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 +306,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 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 +325,30 @@ describe('Testing Impact Service', () => {
expect(axiosSpy).not.toHaveBeenCalled();
});

it('should not send to Impact when isFirstPurchase is false', async () => {
it('When source is direct but coupon code is present, 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 '';
return '';
});
vi.spyOn(localStorageService, 'get').mockImplementation((key) => {
if (key === 'couponCode') return 'CNINTERNXT';
if (key === 'amountPaid') return expectedAmount;
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('');
});

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 +364,7 @@ describe('Testing Impact Service', () => {
});

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

Expand All @@ -378,13 +373,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', 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 error occurs in the entire function, then it handles it gracefully', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(localStorageService, 'getUser').mockImplementation(() => {
throw new Error('Storage Error');
Expand All @@ -399,7 +394,7 @@ describe('Testing Impact Service', () => {
});

describe('uuid library', () => {
it('v4 generates a valid UUID', async () => {
it('When calling v4, then it generates a valid UUID', async () => {
const { v4 } = await vi.importActual<typeof import('uuid')>('uuid');
const id = v4();
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
Expand Down
10 changes: 1 addition & 9 deletions src/app/analytics/impact.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import { sendAddShoppersConversion } from './addShoppers.services';
*
*/
export interface SavePaymentDataParams {
subscriptionId: string | undefined;
paymentIntentId: string | undefined;
selectedPlan: PriceWithTax | undefined;
users: number;
Expand All @@ -38,18 +37,13 @@ export interface SavePaymentDataParams {
}

export function savePaymentDataInLocalStorage({
subscriptionId,
paymentIntentId,
selectedPlan,
users,
couponCodeData,
isFirstPurchase,
}: SavePaymentDataParams) {
if (subscriptionId && selectedPlan?.price.interval !== 'lifetime') {
localStorageService.set('subscriptionId', subscriptionId);
}

if (paymentIntentId && selectedPlan?.price.interval === 'lifetime') {
if (paymentIntentId) {
localStorageService.set('paymentIntentId', paymentIntentId);
}

Expand Down Expand Up @@ -107,7 +101,6 @@ export async function trackPaymentConversion(): Promise<void> {
}

const { uuid, email: userEmail } = userSettings;
const subscription = localStorageService.get('subscriptionId');
const paymentIntent = localStorageService.get('paymentIntentId');
const currency = localStorageService.get('currency');
const amountPaidStr = localStorageService.get('amountPaid');
Expand Down Expand Up @@ -138,7 +131,6 @@ export async function trackPaymentConversion(): Promise<void> {
timestamp: dayjs().format('YYYY-MM-DDTHH:mm:ss.sssZ'),
properties: {
impact_value: amount === 0 ? 0.01 : amount,
subscription_id: subscription,
payment_intent: paymentIntent,
...(couponCode && { order_promo_code: couponCode }),
},
Expand Down
Loading
Loading