From 7d9838913e1ac5003f7d8daafae6be3e0a2deafe Mon Sep 17 00:00:00 2001 From: CamilleFljt Date: Tue, 1 Sep 2026 10:30:39 +0200 Subject: [PATCH 1/2] feat(dcc2): complete eligibility fixtures --- .../__tests__/Schedule.test.tsx | 1 + src/hooks/useFetchEligibility.test.ts | 71 ++++++----- src/test/fixtures.test.ts | 18 +++ src/test/fixtures.ts | 118 ++++++++++++++++++ src/types.ts | 1 + src/utils/index.test.tsx | 1 + 6 files changed, 181 insertions(+), 29 deletions(-) create mode 100644 src/test/fixtures.test.ts diff --git a/src/Widgets/EligibilityModal/__tests__/Schedule.test.tsx b/src/Widgets/EligibilityModal/__tests__/Schedule.test.tsx index 44490489..71ab62d1 100644 --- a/src/Widgets/EligibilityModal/__tests__/Schedule.test.tsx +++ b/src/Widgets/EligibilityModal/__tests__/Schedule.test.tsx @@ -16,6 +16,7 @@ it('should be displayed', async () => { eligible: false, installments_count: 6, purchase_amount: 0, + transaction_country: 'FR', }} />, ) diff --git a/src/hooks/useFetchEligibility.test.ts b/src/hooks/useFetchEligibility.test.ts index 7de73442..d2f92adb 100644 --- a/src/hooks/useFetchEligibility.test.ts +++ b/src/hooks/useFetchEligibility.test.ts @@ -4,26 +4,32 @@ import { ApiMode } from '@/consts' import { statusResponse } from '@/types' import useFetchEligibility from 'hooks/useFetchEligibility' import { useSessionStorage } from 'hooks/useSessionStorage' -import { mockPlansAllEligible } from 'test/fixtures' +import { configPlans, mockPlansAllEligible, withCountry } from 'test/fixtures' import { fetchFromApi } from 'utils/fetch' import filterEligibility from 'utils/filterEligibility' jest.mock('utils/fetch') jest.mock('hooks/useSessionStorage') +// Every test needs the hook's caching layer mocked; only `getCache` varies from one test to another. +const mockSessionStorage = (getCache: jest.Mock = jest.fn()) => { + const mocked = { + getCache, + setCache: jest.fn(), + createKey: jest.fn().mockReturnValue('mocked_key'), + clearCache: jest.fn(), + } + ;(useSessionStorage as jest.Mock).mockReturnValue(mocked) + return mocked +} + describe('useFetchEligibility', () => { beforeEach(() => { jest.clearAllMocks() }) it('should fetch eligibility data and update state', async () => { - const mockedSessionStorage = { - getCache: jest.fn(), - setCache: jest.fn(), - createKey: jest.fn().mockReturnValue('mocked_key'), - clearCache: jest.fn(), - } - ;(useSessionStorage as jest.Mock).mockReturnValue(mockedSessionStorage) + const mockedSessionStorage = mockSessionStorage() ;(fetchFromApi as jest.Mock).mockImplementation(async () => mockPlansAllEligible) const { result } = renderHook(() => @@ -68,13 +74,7 @@ describe('useFetchEligibility', () => { it('should use cached data if available to avoid calling fetch again', async () => { // Not using directly mockPlansAllEligible to make sure we gather the results stored in the cache const newMockedResult = [mockPlansAllEligible[0]] - const mockedSessionStorage = { - getCache: jest.fn().mockReturnValue({ key: 'mocked_key', value: newMockedResult }), - setCache: jest.fn(), - createKey: jest.fn().mockReturnValue('mocked_key'), - clearCache: jest.fn(), - } - ;(useSessionStorage as jest.Mock).mockReturnValue(mockedSessionStorage) + mockSessionStorage(jest.fn().mockReturnValue({ key: 'mocked_key', value: newMockedResult })) const { result } = renderHook(() => useFetchEligibility( @@ -97,13 +97,7 @@ describe('useFetchEligibility', () => { }) it('should returns a FAILED status if the API response contains `error_code`', async () => { - const mockedSessionStorage = { - getCache: jest.fn(), - setCache: jest.fn(), - createKey: jest.fn().mockReturnValue('mocked_key'), - clearCache: jest.fn(), - } - ;(useSessionStorage as jest.Mock).mockReturnValue(mockedSessionStorage) + const mockedSessionStorage = mockSessionStorage() ;(fetchFromApi as jest.Mock).mockImplementation(async () => ({ message: 'some error', error_code: '403', @@ -129,13 +123,7 @@ describe('useFetchEligibility', () => { expect(result.current[0]).toEqual([]) }) it('should returns a FAILED status if the API response contains `errors`', async () => { - const mockedSessionStorage = { - getCache: jest.fn(), - setCache: jest.fn(), - createKey: jest.fn().mockReturnValue('mocked_key'), - clearCache: jest.fn(), - } - ;(useSessionStorage as jest.Mock).mockReturnValue(mockedSessionStorage) + const mockedSessionStorage = mockSessionStorage() ;(fetchFromApi as jest.Mock).mockImplementation(async () => ({ errors: 'some error', })) @@ -159,4 +147,29 @@ describe('useFetchEligibility', () => { // The hook response should be empty expect(result.current[0]).toEqual([]) }) + it('should expose the transaction_country returned by the API, not the requested country', async () => { + mockSessionStorage() + // The API answers with an Italian transaction while the customer addresses are French, so a + // plan carrying `IT` can only come from the response itself. Which plan it is does not matter. + const [anyEligiblePlan] = mockPlansAllEligible + const planInItaly = withCountry(anyEligiblePlan, 'IT') + ;(fetchFromApi as jest.Mock).mockImplementation(async () => [planInItaly]) + + const { result } = renderHook(() => + // `configPlans` is required: it is the branch of filterEligibility that rebuilds each plan + // object, and therefore the only place where the new field could get dropped. + useFetchEligibility( + 45000, + { domain: ApiMode.TEST, merchantId: 'test_id' }, + configPlans, + 'FR', + 'FR', + ), + ) + + await waitFor(() => { + expect(result.current[0]).toHaveLength(1) + }) + expect(result.current[0][0].transaction_country).toBe('IT') + }) }) diff --git a/src/test/fixtures.test.ts b/src/test/fixtures.test.ts new file mode 100644 index 00000000..c000b6b0 --- /dev/null +++ b/src/test/fixtures.test.ts @@ -0,0 +1,18 @@ +import { mockDeferredP1XPlan, withCountry } from 'test/fixtures' + +describe('withCountry', () => { + it('should return a copy of the plan with transaction_country overridden', () => { + expect(withCountry(mockDeferredP1XPlan, 'IT')).toEqual({ + ...mockDeferredP1XPlan, + transaction_country: 'IT', + }) + }) + + it('should not mutate the given plan', () => { + const plan = { ...mockDeferredP1XPlan } + + withCountry(plan, 'ES') + + expect(plan).toEqual(mockDeferredP1XPlan) + }) +}) diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts index 7a0c1551..e3222d14 100644 --- a/src/test/fixtures.ts +++ b/src/test/fixtures.ts @@ -18,6 +18,7 @@ export const mockPlansAllEligible: EligibilityPlan[] = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { customer_total_cost_amount: 0, @@ -36,6 +37,7 @@ export const mockPlansAllEligible: EligibilityPlan[] = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { customer_total_cost_amount: 0, @@ -61,6 +63,7 @@ export const mockPlansAllEligible: EligibilityPlan[] = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { customer_total_cost_amount: 135, @@ -93,6 +96,7 @@ export const mockPlansAllEligible: EligibilityPlan[] = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { customer_total_cost_amount: 1062, @@ -132,6 +136,7 @@ export const mockPlansAllEligible: EligibilityPlan[] = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { annual_interest_rate: 1720, @@ -224,6 +229,7 @@ export const mockPlansAllEligible: EligibilityPlan[] = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, ] @@ -245,6 +251,7 @@ export const mockButtonPlans = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { customer_total_cost_amount: 0, @@ -263,6 +270,7 @@ export const mockButtonPlans = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { customer_total_cost_amount: 0, @@ -288,6 +296,7 @@ export const mockButtonPlans = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { customer_total_cost_amount: 135, @@ -320,6 +329,7 @@ export const mockButtonPlans = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { customer_total_cost_amount: 1202, @@ -359,6 +369,7 @@ export const mockButtonPlans = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { annual_interest_rate: 1719, @@ -451,6 +462,7 @@ export const mockButtonPlans = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, ] @@ -474,6 +486,7 @@ export const mockEligibilityPaymentPlanWithIneligiblePlan = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { customer_fee: 0, @@ -501,6 +514,7 @@ export const mockEligibilityPaymentPlanWithIneligiblePlan = [ }, ], purchase_amount: 45000, + transaction_country: 'FR', }, { constraints: { purchase_amount: { maximum: 20000, minimum: 9000 } }, @@ -509,6 +523,7 @@ export const mockEligibilityPaymentPlanWithIneligiblePlan = [ eligible: false, installments_count: 4, purchase_amount: 45000, + transaction_country: 'FR', reasons: { purchase_amount: 'invalid_value' }, }, { @@ -518,6 +533,7 @@ export const mockEligibilityPaymentPlanWithIneligiblePlan = [ eligible: false, installments_count: 10, purchase_amount: 45000, + transaction_country: 'FR', reasons: { purchase_amount: 'invalid_value' }, }, ] @@ -533,6 +549,7 @@ export const mockEligibilityWithHiddenPlan = [ eligible: false, installments_count: 4, purchase_amount: 45000, + transaction_country: 'FR', reasons: { installments_count: 'not_allowed' }, // No constraints.purchase_amount → plan is hidden, not grayed out }, @@ -549,6 +566,7 @@ export const mockEligibilityWithGrayedOutPlan = [ eligible: false, installments_count: 4, purchase_amount: 45000, + transaction_country: 'FR', reasons: { purchase_amount: 'invalid_value' }, // constraints.purchase_amount present → plan is grayed out, not hidden }, @@ -569,3 +587,103 @@ export const configPlans: ConfigPlan[] = mockPlansAllEligible.map((plan) => ({ minAmount: 90_00, maxAmount: 3350_00, })) + +// Deferred (30 days) 3x plan, without customer fees. +export const mockDeferredMultiInstallmentPlanWithoutFees: EligibilityPlan = { + customer_total_cost_amount: 0, + customer_total_cost_bps: 0, + deferred_days: 30, + deferred_months: 0, + eligible: true, + installments_count: 3, + payment_plan: [ + { + customer_fee: 0, + customer_interest: 0, + due_date: 1640942762, + purchase_amount: 15000, + total_amount: 15000, + }, + { + customer_fee: 0, + customer_interest: 0, + due_date: 1643621162, + purchase_amount: 15000, + total_amount: 15000, + }, + { + customer_fee: 0, + customer_interest: 0, + due_date: 1646299562, + purchase_amount: 15000, + total_amount: 15000, + }, + ], + purchase_amount: 45000, + transaction_country: 'FR', +} + +// Same plan as above, with customer fees charged on the first installment. +export const mockDeferredMultiInstallmentPlanWithFees: EligibilityPlan = { + customer_total_cost_amount: 135, + customer_total_cost_bps: 30, + deferred_days: 30, + deferred_months: 0, + eligible: true, + installments_count: 3, + payment_plan: [ + { + customer_fee: 135, + customer_interest: 0, + due_date: 1640942762, + purchase_amount: 15000, + total_amount: 15135, + }, + { + customer_fee: 0, + customer_interest: 0, + due_date: 1643621162, + purchase_amount: 15000, + total_amount: 15000, + }, + { + customer_fee: 0, + customer_interest: 0, + due_date: 1646299562, + purchase_amount: 15000, + total_amount: 15000, + }, + ], + purchase_amount: 45000, + transaction_country: 'FR', +} + +// Deferred P1X: a single installment paid 30 days later (pay later). +export const mockDeferredP1XPlan: EligibilityPlan = { + customer_total_cost_amount: 0, + customer_total_cost_bps: 0, + deferred_days: 30, + deferred_months: 0, + eligible: true, + installments_count: 1, + payment_plan: [ + { + customer_fee: 0, + customer_interest: 0, + due_date: 1640942762, + purchase_amount: 45000, + total_amount: 45000, + }, + ], + purchase_amount: 45000, + transaction_country: 'FR', +} + +/** + * Returns a copy of `plan` with its `transaction_country` overridden. + * The input plan is never mutated, so shared fixtures stay reusable across tests. + */ +export const withCountry = (plan: T, countryCode: string): T => ({ + ...plan, + transaction_country: countryCode, +}) diff --git a/src/types.ts b/src/types.ts index 2f598bd7..8baeb48c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -44,6 +44,7 @@ export type EligibilityPlan = { constraints?: { purchase_amount?: { minimum: number; maximum: number } } + transaction_country: string } export type ErrorResponse = { diff --git a/src/utils/index.test.tsx b/src/utils/index.test.tsx index a6eb936d..5ab4f533 100644 --- a/src/utils/index.test.tsx +++ b/src/utils/index.test.tsx @@ -94,6 +94,7 @@ describe('utils', () => { eligible: true, // This part is important installments_count: 10, purchase_amount: 1000, + transaction_country: 'FR', // Also no payment-plan }), ).toThrow( From b5f742bf1375798ec0883683fc6fad4f8b947332 Mon Sep 17 00:00:00 2001 From: CamilleFljt Date: Tue, 1 Sep 2026 17:44:56 +0200 Subject: [PATCH 2/2] feat(dcc2): clean code --- src/hooks/useFetchEligibility.test.ts | 3 ++- src/test/fixtures.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hooks/useFetchEligibility.test.ts b/src/hooks/useFetchEligibility.test.ts index d2f92adb..d5eaa403 100644 --- a/src/hooks/useFetchEligibility.test.ts +++ b/src/hooks/useFetchEligibility.test.ts @@ -168,8 +168,9 @@ describe('useFetchEligibility', () => { ) await waitFor(() => { - expect(result.current[0]).toHaveLength(1) + expect(result.current[1]).toBe(statusResponse.SUCCESS) }) + expect(result.current[0]).toHaveLength(1) expect(result.current[0][0].transaction_country).toBe('IT') }) }) diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts index e3222d14..a4c90ce7 100644 --- a/src/test/fixtures.ts +++ b/src/test/fixtures.ts @@ -683,7 +683,7 @@ export const mockDeferredP1XPlan: EligibilityPlan = { * Returns a copy of `plan` with its `transaction_country` overridden. * The input plan is never mutated, so shared fixtures stay reusable across tests. */ -export const withCountry = (plan: T, countryCode: string): T => ({ +export const withCountry = (plan: EligibilityPlan, countryCode: string): EligibilityPlan => ({ ...plan, transaction_country: countryCode, })