Skip to content
Merged
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
77 changes: 72 additions & 5 deletions src/app/analytics/impact.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ beforeEach(() => {
if (key === 'currency') return product.price.currency;
if (key === 'amountPaid') return expectedAmount;
if (key === 'couponCode') return promoCode.codeName;
if (key === 'isFirstPurchase') return 'true';
return null;
});
});
Expand All @@ -108,15 +109,29 @@ describe('Testing Impact Service', () => {
it('should save the correct amount to localStorage after applying coupon', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

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

expect(setToLocalStorageSpy).toHaveBeenCalledWith('amountPaid', expectedAmount);
});

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

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

expect(setToLocalStorageSpy).toHaveBeenCalledWith('subscriptionId', subId);
});
Expand All @@ -128,15 +143,29 @@ describe('Testing Impact Service', () => {
price: { ...product.price, interval: 'lifetime' },
};

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

expect(setToLocalStorageSpy).toHaveBeenCalledWith('paymentIntentId', paymentIntentId);
});

it('should save product metadata including name, price ID, and currency', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

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

expect(setToLocalStorageSpy).toHaveBeenCalledWith('productName', planName);
expect(setToLocalStorageSpy).toHaveBeenCalledWith('priceId', product.price.id);
Expand All @@ -146,10 +175,32 @@ describe('Testing Impact Service', () => {
it('should save coupon code when provided', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

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

expect(setToLocalStorageSpy).toHaveBeenCalledWith('couponCode', promoCode.codeName);
});

it('should save isFirstPurchase flag to localStorage', () => {
const setToLocalStorageSpy = vi.spyOn(localStorageService, 'set');

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

expect(setToLocalStorageSpy).toHaveBeenCalledWith('isFirstPurchase', 'true');
});
});

describe('trackSignUp', () => {
Expand Down Expand Up @@ -252,6 +303,7 @@ describe('Testing Impact Service', () => {
if (key === 'amountPaid') return '0';
if (key === 'subscriptionId') return subId;
if (key === 'couponCode') return promoCode.codeName;
if (key === 'isFirstPurchase') return 'true';
return null;
});
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});
Expand Down Expand Up @@ -291,6 +343,21 @@ describe('Testing Impact Service', () => {
vi.spyOn(localStorageService, 'get').mockImplementation((key) => {
if (key === 'couponCode') return null;
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 not send to Impact when isFirstPurchase is false', async () => {
vi.spyOn(localStorageService, 'get').mockImplementation((key) => {
if (key === 'isFirstPurchase') return 'false';
if (key === 'amountPaid') return expectedAmount;
if (key === 'couponCode') return promoCode.codeName;
return null;
});
const axiosSpy = vi.spyOn(axios, 'post').mockResolvedValue({});
Expand Down
34 changes: 21 additions & 13 deletions src/app/analytics/impact.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,24 @@ import { sendAddShoppersConversion } from './addShoppers.services';
* - Number of users
* - Coupon code data (if any)
*
* @param subscriptionId - Stripe subscription ID (only for recurring plans)
* @param paymentIntentId - Stripe payment intent ID (only for lifetime plans)
* @param selectedPlan - The pricing plan selected by the user
* @param users - Number of users for the purchase (1 for individual, >1 for B2B)
* @param couponCodeData - Optional coupon code information applied to the purchase
*/
export function savePaymentDataInLocalStorage(
subscriptionId: string | undefined,
paymentIntentId: string | undefined,
selectedPlan: PriceWithTax | undefined,
users: number,
couponCodeData: CouponCodeData | undefined,
) {
export interface SavePaymentDataParams {
subscriptionId: string | undefined;
paymentIntentId: string | undefined;
selectedPlan: PriceWithTax | undefined;
users: number;
couponCodeData: CouponCodeData | undefined;
isFirstPurchase: boolean;
}

export function savePaymentDataInLocalStorage({
subscriptionId,
paymentIntentId,
selectedPlan,
users,
couponCodeData,
isFirstPurchase,
}: SavePaymentDataParams) {
if (subscriptionId && selectedPlan?.price.interval !== 'lifetime') {
localStorageService.set('subscriptionId', subscriptionId);
}
Expand All @@ -61,6 +66,8 @@ export function savePaymentDataInLocalStorage(
if (couponCodeData?.codeName) {
localStorageService.set('couponCode', couponCodeData.codeName);
}

localStorageService.set('isFirstPurchase', String(isFirstPurchase));
}

export async function trackSignUp(uuid: string): Promise<void> {
Expand Down Expand Up @@ -106,6 +113,7 @@ export async function trackPaymentConversion(): Promise<void> {
const amountPaidStr = localStorageService.get('amountPaid');
const amount = Number.parseFloat(amountPaidStr ?? '0');
const couponCode = localStorageService.get('couponCode');
const isFirstPurchase = localStorageService.get('isFirstPurchase') === 'true';

try {
sendAddShoppersConversion({
Expand All @@ -123,7 +131,7 @@ export async function trackPaymentConversion(): Promise<void> {
const anonymousID = getCookie('impactAnonymousId');
const source = getCookie('impactSource');

if ((source && source !== 'direct') || couponCode) {
if (isFirstPurchase && ((source && source !== 'direct') || couponCode)) {
try {
await axios.post(IMPACT_API, {
anonymousId: anonymousID,
Expand Down
47 changes: 41 additions & 6 deletions src/views/Checkout/hooks/useUserPayment.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { useUserPayment } from './useUserPayment';
import { checkIsFirstPurchase, useUserPayment } from './useUserPayment';
import checkoutService from '../services/checkout.service';
import localStorageService from 'services/local-storage.service';
import envService from 'services/env.service';
Expand All @@ -9,6 +9,8 @@ import { AppView } from 'app/core/types';
import { PaymentType, ProcessPurchasePayload, UseUserPaymentPayload } from '../types';
import { CreateSubscriptionPayload } from '@internxt/sdk/dist/payments/types';
import notificationsService from 'app/notifications/services/notifications.service';
import { paymentService } from '../services';
import errorService from 'services/error.service';

const mockHostname = 'https://hostname.com';

Expand All @@ -24,6 +26,8 @@ describe('Custom hook to handle payments', () => {
if (key === 'hostname') return mockHostname;
else return 'no mock implementation';
});

vi.spyOn(paymentService, 'getInvoices').mockResolvedValue([]);
});

describe('Get subscription data to do the payment', () => {
Expand Down Expand Up @@ -190,7 +194,7 @@ describe('Custom hook to handle payments', () => {
captchaToken: subscriptionPaymentPayload.captchaToken,
});

expect(localStorageServiceSpy).toHaveBeenCalledTimes(5);
expect(localStorageServiceSpy).toHaveBeenCalledTimes(6);

expect(setupIntent).toHaveBeenCalledWith({
elements: subscriptionPaymentPayload.elements,
Expand Down Expand Up @@ -248,7 +252,7 @@ describe('Custom hook to handle payments', () => {
captchaToken: subscriptionPaymentPayload.captchaToken,
});

expect(localStorageServiceSpy).toHaveBeenCalledTimes(5);
expect(localStorageServiceSpy).toHaveBeenCalledTimes(6);

expect(confirmPayment).toHaveBeenCalledWith({
elements: subscriptionPaymentPayload.elements,
Expand Down Expand Up @@ -307,7 +311,7 @@ describe('Custom hook to handle payments', () => {
captchaToken: subscriptionPaymentPayload.captchaToken,
});

expect(localStorageServiceSpy).toHaveBeenCalledTimes(5);
expect(localStorageServiceSpy).toHaveBeenCalledTimes(6);

expect(notificationsServiceSpy).toHaveBeenCalled();
expect(confirmPayment).not.toHaveBeenCalled();
Expand Down Expand Up @@ -356,7 +360,7 @@ describe('Custom hook to handle payments', () => {
promoCodeId: undefined,
});

expect(localStorageServiceSpy).toHaveBeenCalledTimes(5);
expect(localStorageServiceSpy).toHaveBeenCalledTimes(6);

expect(confirmPayment).toHaveBeenCalledWith({
elements: lifetimePaymentPayload.elements,
Expand Down Expand Up @@ -408,13 +412,44 @@ describe('Custom hook to handle payments', () => {
promoCodeId: undefined,
});

expect(localStorageServiceSpy).toHaveBeenCalledTimes(5);
expect(localStorageServiceSpy).toHaveBeenCalledTimes(6);
expect(navigationServiceSpy).toHaveBeenCalledWith(AppView.CheckoutSuccess);

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

describe('checkIsFirstPurchase', () => {
test('When the user has no prior invoices, then it is a first purchase', async () => {
vi.spyOn(paymentService, 'getInvoices').mockResolvedValue([]);

const result = await checkIsFirstPurchase();

expect(result).toBe(true);
expect(paymentService.getInvoices).toHaveBeenCalledWith({ userType: UserType.Individual, limit: 1 });
});

test('When the user has prior invoices, then it is not a first purchase', async () => {
vi.spyOn(paymentService, 'getInvoices').mockResolvedValue([{ id: 'inv_123' } as any]);

const result = await checkIsFirstPurchase();

expect(result).toBe(false);
});

test('When getInvoices fails, then it returns false and reports the error', async () => {
const error = new Error('Network error');
vi.spyOn(paymentService, 'getInvoices').mockRejectedValue(error);
const reportErrorSpy = vi.spyOn(errorService, 'reportError').mockImplementation(() => {});
vi.spyOn(errorService, 'castError').mockReturnValue(error as any);

const result = await checkIsFirstPurchase();

expect(result).toBe(false);
expect(reportErrorSpy).toHaveBeenCalledWith(error);
});
});

describe('Handling user payment', () => {
test('When the plan is a subscription, then the subscription handler is called', async () => {
const payment = useUserPayment();
Expand Down
Loading
Loading