From 8a35c7e6a6e4acf2c54e4ca1973645db1ac463ea Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Thu, 23 Apr 2026 12:15:09 +0200 Subject: [PATCH 1/7] fix: resolve missing impact attribution for coupon-only sales --- src/app/analytics/impact.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/analytics/impact.service.ts b/src/app/analytics/impact.service.ts index a0ff1268a5..3b90a18c1d 100644 --- a/src/app/analytics/impact.service.ts +++ b/src/app/analytics/impact.service.ts @@ -128,7 +128,7 @@ export async function trackPaymentConversion(): Promise { } 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)) { @@ -140,7 +140,7 @@ export async function trackPaymentConversion(): Promise { impact_value: amount === 0 ? 0.01 : amount, subscription_id: subscription, payment_intent: paymentIntent, - ...(couponCode && { order_promo_code: couponCode }), + ...(couponCode && { order_promo_code: couponCode.toUpperCase() }), }, userId: uuid, type: 'track', From 6752b289951c4e6c1793b09b85299ab8daf02dbb Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Thu, 23 Apr 2026 12:17:59 +0200 Subject: [PATCH 2/7] fix: resolve missing impact attribution for coupon-only sales --- src/app/analytics/impact.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/analytics/impact.service.ts b/src/app/analytics/impact.service.ts index 3b90a18c1d..5a9cfe76f4 100644 --- a/src/app/analytics/impact.service.ts +++ b/src/app/analytics/impact.service.ts @@ -140,7 +140,7 @@ export async function trackPaymentConversion(): Promise { impact_value: amount === 0 ? 0.01 : amount, subscription_id: subscription, payment_intent: paymentIntent, - ...(couponCode && { order_promo_code: couponCode.toUpperCase() }), + ...(couponCode && { order_promo_code: couponCode }), }, userId: uuid, type: 'track', From e42cf8c6c7416ef22dbcb41e6752764a21e0cc65 Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Thu, 23 Apr 2026 16:06:55 +0200 Subject: [PATCH 3/7] fix: resolve missing impact attribution for coupon-only sales --- src/app/analytics/impact.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/analytics/impact.service.ts b/src/app/analytics/impact.service.ts index 5a9cfe76f4..51bac7b690 100644 --- a/src/app/analytics/impact.service.ts +++ b/src/app/analytics/impact.service.ts @@ -131,7 +131,10 @@ export async function trackPaymentConversion(): Promise { 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, From 2e3c43e7da336b1689cf0bf1a4603d2e788f015d Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Thu, 23 Apr 2026 16:19:17 +0200 Subject: [PATCH 4/7] fix: resolve missing impact attribution for coupon-only sales --- src/app/analytics/impact.service.test.ts | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/app/analytics/impact.service.test.ts b/src/app/analytics/impact.service.test.ts index d7b7cd352f..2e603a3078 100644 --- a/src/app/analytics/impact.service.test.ts +++ b/src/app/analytics/impact.service.test.ts @@ -353,6 +353,49 @@ describe('Testing Impact Service', () => { expect(axiosSpy).not.toHaveBeenCalled(); }); + it('should not send to Impact when source is direct and coupon code is not in whitelist', 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('should send to Impact when source is direct but coupon code is in whitelist', 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; anonymousId: string }; + expect(callArgs.properties).toHaveProperty('order_promo_code', 'CNINTERNXT'); + expect(callArgs.anonymousId).toBe(mockedUserUuid); // Fallback to uuidV4 + }); + it('should not send to Impact when isFirstPurchase is false', async () => { vi.spyOn(localStorageService, 'get').mockImplementation((key) => { if (key === 'isFirstPurchase') return 'false'; From 70ab2cd8c21c4f74f62d1d6a96bfa81cd1836866 Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Thu, 23 Apr 2026 16:33:58 +0200 Subject: [PATCH 5/7] test(analytics): fix broken tests due to whitelist blocking generic PROMO string --- src/app/analytics/impact.service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/analytics/impact.service.test.ts b/src/app/analytics/impact.service.test.ts index 2e603a3078..e6eb351c7e 100644 --- a/src/app/analytics/impact.service.test.ts +++ b/src/app/analytics/impact.service.test.ts @@ -51,7 +51,7 @@ const promoCode = { amountOff: undefined, codeId: 'promo_123', percentOff: 99, - codeName: 'PROMO', + codeName: 'CNINTERNXTL', }; const product = { From 10c9b86f1dff2892f8f69c5e96b509321d746815 Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Fri, 24 Apr 2026 11:55:08 +0200 Subject: [PATCH 6/7] Update impact.service.test.ts --- src/app/analytics/impact.service.test.ts | 44 ++++++++++++------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/app/analytics/impact.service.test.ts b/src/app/analytics/impact.service.test.ts index e6eb351c7e..733536d266 100644 --- a/src/app/analytics/impact.service.test.ts +++ b/src/app/analytics/impact.service.test.ts @@ -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({ @@ -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({ @@ -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, @@ -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({ @@ -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({ @@ -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({ @@ -205,7 +205,7 @@ 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); @@ -213,7 +213,7 @@ describe('Testing Impact Service', () => { 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; @@ -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); @@ -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); @@ -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'; @@ -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(); @@ -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; @@ -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(); @@ -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'); @@ -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'; @@ -353,7 +353,7 @@ describe('Testing Impact Service', () => { expect(axiosSpy).not.toHaveBeenCalled(); }); - it('should not send to Impact when source is direct and coupon code is not in whitelist', 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'; @@ -372,7 +372,7 @@ describe('Testing Impact Service', () => { expect(axiosSpy).not.toHaveBeenCalled(); }); - it('should send to Impact when source is direct but coupon code is in whitelist', async () => { + 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'; @@ -396,7 +396,7 @@ describe('Testing Impact Service', () => { expect(callArgs.anonymousId).toBe(mockedUserUuid); // Fallback to uuidV4 }); - it('should not send to Impact when isFirstPurchase is false', async () => { + 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; @@ -412,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); @@ -421,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'); From c0a86d752d0e57c4f20686b607889555685f696b Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Mon, 11 May 2026 10:59:02 +0200 Subject: [PATCH 7/7] add new coupon code and improve test --- src/app/analytics/impact.service.test.ts | 2 +- src/app/analytics/impact.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/analytics/impact.service.test.ts b/src/app/analytics/impact.service.test.ts index b475559be7..495fb3c97d 100644 --- a/src/app/analytics/impact.service.test.ts +++ b/src/app/analytics/impact.service.test.ts @@ -442,7 +442,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('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); diff --git a/src/app/analytics/impact.service.ts b/src/app/analytics/impact.service.ts index f8bfb8a9f5..a1a20e605d 100644 --- a/src/app/analytics/impact.service.ts +++ b/src/app/analytics/impact.service.ts @@ -131,7 +131,7 @@ export async function trackPaymentConversion(): Promise { const anonymousID = getCookie('impactAnonymousId') || uuidV4(); const source = getCookie('impactSource'); - const IMPACT_COUPON_WHITELIST = ['CNINTERNXT', 'CNINTERNXTL']; + const IMPACT_COUPON_WHITELIST = ['CNINTERNXT', 'CNINTERNXTL', 'CLOUDOFF']; const isImpactCoupon = couponCode && IMPACT_COUPON_WHITELIST.includes(couponCode.toUpperCase()); if (isFirstPurchase && ((source && source !== 'direct') || isImpactCoupon)) {