From 110b192c82aded0c7c2a60edb0af320e7ba34a91 Mon Sep 17 00:00:00 2001 From: ghorbani Date: Tue, 11 Aug 2026 13:04:37 +0330 Subject: [PATCH] feat: implement question navigation system and multi-language support --- src/app/questions-list/page.tsx | 4 +- src/app/terms/page.test.tsx | 100 +++++++ src/app/terms/page.tsx | 59 ++++ src/components/Componentes/question-date.tsx | 24 +- .../question-exit-navigation-button.tsx | 18 +- .../Componentes/question-section-flow.tsx | 12 +- .../Componentes/slider-page.test.tsx | 265 ++++++++++++++++++ src/components/Componentes/slider-page.tsx | 85 ++++-- src/lib/get-submit-path.test.ts | 88 ++++++ src/translations/locales/ar.json | 5 +- src/translations/locales/az.json | 5 +- src/translations/locales/bn.json | 5 +- src/translations/locales/da.json | 5 +- src/translations/locales/de.json | 5 +- src/translations/locales/en.json | 6 +- src/translations/locales/es.json | 5 +- src/translations/locales/fa.json | 6 +- src/translations/locales/fr.json | 5 +- src/translations/locales/gu.json | 5 +- src/translations/locales/ha.json | 5 +- src/translations/locales/he.json | 5 +- src/translations/locales/hi.json | 5 +- src/translations/locales/id.json | 5 +- src/translations/locales/ks.json | 5 +- src/translations/locales/pt.json | 5 +- src/translations/locales/ru.json | 5 +- src/translations/locales/sw.json | 5 +- src/translations/locales/tg.json | 5 +- src/translations/locales/tr.json | 5 +- src/translations/locales/ul.json | 5 +- src/translations/locales/ur.json | 5 +- src/translations/locales/uz.json | 5 +- src/translations/locales/zh.json | 5 +- 33 files changed, 672 insertions(+), 105 deletions(-) create mode 100644 src/app/terms/page.test.tsx create mode 100644 src/components/Componentes/slider-page.test.tsx create mode 100644 src/lib/get-submit-path.test.ts diff --git a/src/app/questions-list/page.tsx b/src/app/questions-list/page.tsx index 11bfa7e..6e15718 100644 --- a/src/app/questions-list/page.tsx +++ b/src/app/questions-list/page.tsx @@ -79,7 +79,9 @@ export default function QuestionsListPage() { // Add fallback for combined section from the schema adapter which attaches it to questionListItems directly questionListItems.forEach((item) => { - progressBySlug.set(item.slug, item.progress); + if (!progressBySlug.has(item.slug)) { + progressBySlug.set(item.slug, item.progress); + } }); return progressBySlug; diff --git a/src/app/terms/page.test.tsx b/src/app/terms/page.test.tsx new file mode 100644 index 0000000..2a993ba --- /dev/null +++ b/src/app/terms/page.test.tsx @@ -0,0 +1,100 @@ +import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import TermsRoute from './page'; + +const mockUseMarriageProfileQuery = vi.fn(); +const mockReplace = vi.fn(); + +vi.mock('@/hooks/marriage/use-profile-main', () => ({ + useMarriageProfileQuery: () => mockUseMarriageProfileQuery(), +})); + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + replace: mockReplace, + }), +})); + +vi.mock('@/translations/provider', () => ({ + useI18n: () => ({ + locale: 'en', + dictionary: { + "Failed to load profile. Please try again.": "Failed to load profile. Please try again.", + "Retry": "Retry", + }, + }), +})); + +vi.mock('@/translations/config', () => ({ + localizePath: (path: string) => path, +})); + +vi.mock('@/components/Componentes/slider-page', () => ({ + default: () =>
SliderPage
, +})); + +describe('TermsRoute Guard', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + cleanup(); + }); + + it('renders loading state when isLoading is true', () => { + mockUseMarriageProfileQuery.mockReturnValue({ isLoading: true }); + render(); + expect(screen.queryByTestId('slider-page')).not.toBeInTheDocument(); + // The spinner should be visible, maybe just check for a div. We can check slider is absent. + }); + + it('renders Error/Retry when isError is true', () => { + const mockRefetch = vi.fn(); + mockUseMarriageProfileQuery.mockReturnValue({ isError: true, refetch: mockRefetch }); + render(); + + expect(screen.getByText('Failed to load profile. Please try again.')).toBeInTheDocument(); + const retryBtn = screen.getByText('Retry'); + fireEvent.click(retryBtn); + expect(mockRefetch).toHaveBeenCalled(); + expect(screen.queryByTestId('slider-page')).not.toBeInTheDocument(); + }); + + it('pending_onboarding => SliderPage renders', () => { + mockUseMarriageProfileQuery.mockReturnValue({ + isLoading: false, + data: { status: 'pending_onboarding' } + }); + render(); + + expect(screen.getByTestId('slider-page')).toBeInTheDocument(); + expect(mockReplace).not.toHaveBeenCalled(); + }); + + it('pending_info => redirects to Questions and SliderPage is NOT rendered', async () => { + mockUseMarriageProfileQuery.mockReturnValue({ + isLoading: false, + data: { status: 'pending_info' } + }); + render(); + + expect(screen.queryByTestId('slider-page')).not.toBeInTheDocument(); + await waitFor(() => { + expect(mockReplace).toHaveBeenCalled(); // Should redirect to /questions-list or /questions-list/personal_info + }); + }); + + it('waiting => redirects to Finding/Waiting Page and SliderPage is NOT rendered', async () => { + mockUseMarriageProfileQuery.mockReturnValue({ + isLoading: false, + data: { status: 'waiting' } + }); + render(); + + expect(screen.queryByTestId('slider-page')).not.toBeInTheDocument(); + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/finding-match'); + }); + }); +}); diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx index d308264..a76607c 100644 --- a/src/app/terms/page.tsx +++ b/src/app/terms/page.tsx @@ -1,5 +1,64 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; +import { getSubmitPath } from "@/lib/get-submit-path"; import SliderPage from "@/components/Componentes/slider-page"; +import Button from "@/components/Componentes/button"; +import { useI18n } from "@/translations/provider"; +import { localizePath } from "@/translations/config"; export default function TermsRoute() { + const { data: profile, isLoading, isError, refetch } = useMarriageProfileQuery(); + const router = useRouter(); + const { locale, dictionary: t } = useI18n(); + + useEffect(() => { + if (!isLoading && profile) { + if (profile.status !== "pending_onboarding") { + const target = getSubmitPath(profile); + if (target !== "/terms") { + router.replace(localizePath(target, locale)); + } + } + } + }, [profile, isLoading, router, locale]); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (isError) { + return ( +
+

+ {/* @ts-expect-error - missing key */} + {t["Failed to load profile. Please try again."] || "Failed to load profile. Please try again."} +

+
+ +
+
+ ); + } + + if (profile && profile.status !== "pending_onboarding") { + // If target is /terms but status is not pending_onboarding, we should still not render SliderPage + // to prevent showing Onboarding to users who already finished it but have an unknown status. + return ( +
+
+
+ ); + } + return ; } diff --git a/src/components/Componentes/question-date.tsx b/src/components/Componentes/question-date.tsx index 07de95f..6b23ac4 100644 --- a/src/components/Componentes/question-date.tsx +++ b/src/components/Componentes/question-date.tsx @@ -12,18 +12,18 @@ type QuestionDateProps = { }; const MONTHS = [ - { value: "01", label: "01 - January" }, - { value: "02", label: "02 - February" }, - { value: "03", label: "03 - March" }, - { value: "04", label: "04 - April" }, - { value: "05", label: "05 - May" }, - { value: "06", label: "06 - June" }, - { value: "07", label: "07 - July" }, - { value: "08", label: "08 - August" }, - { value: "09", label: "09 - September" }, - { value: "10", label: "10 - October" }, - { value: "11", label: "11 - November" }, - { value: "12", label: "12 - December" }, + { value: "01", label: "January" }, + { value: "02", label: "February" }, + { value: "03", label: "March" }, + { value: "04", label: "April" }, + { value: "05", label: "May" }, + { value: "06", label: "June" }, + { value: "07", label: "July" }, + { value: "08", label: "August" }, + { value: "09", label: "September" }, + { value: "10", label: "October" }, + { value: "11", label: "November" }, + { value: "12", label: "December" }, ]; const DAYS = Array.from({ length: 31 }, (_, i) => { diff --git a/src/components/Componentes/question-exit-navigation-button.tsx b/src/components/Componentes/question-exit-navigation-button.tsx index 368cbd8..fa34631 100644 --- a/src/components/Componentes/question-exit-navigation-button.tsx +++ b/src/components/Componentes/question-exit-navigation-button.tsx @@ -25,28 +25,32 @@ export function QuestionExitNavigationButton({ const { locale } = useI18n(); const { flushAnswers } = useQuestionAnswers(); + const [isNavigating, setIsNavigating] = useState(false); + return ( { + disabled={props.disabled || isNavigating} + onClick={async (event) => { props.onClick?.(event); - if (event.defaultPrevented) { + if (event.defaultPrevented || isNavigating) { return; } event.preventDefault(); + setIsNavigating(true); try { markFirstEntryCompleted(); - void flushAnswers({ force: true }); + await flushAnswers({ force: true }); } catch { // ignore + } finally { + triggerSilentReload(queryClient); + const target = localizePath(exitHref || "/questions-list", locale); + router.push(target); } - - triggerSilentReload(queryClient); - const target = localizePath(exitHref || "/questions-list", locale); - router.push(target); }} /> ); diff --git a/src/components/Componentes/question-section-flow.tsx b/src/components/Componentes/question-section-flow.tsx index 0561fa0..dee33c9 100644 --- a/src/components/Componentes/question-section-flow.tsx +++ b/src/components/Componentes/question-section-flow.tsx @@ -54,7 +54,7 @@ function SectionFlowContent({ void flushAnswers({ force: true }); }, [flushAnswers]); - const handleSubmit = useCallback(() => { + const handleSubmit = useCallback(async () => { if (isSubmitting) { return; } @@ -62,14 +62,14 @@ function SectionFlowContent({ try { markFirstEntryCompleted(); - void flushAnswers({ force: true }); + await flushAnswers({ force: true }); } catch { // ignore + } finally { + triggerSilentReload(queryClient); + const target = localizePath(exitHref || "/questions-list", locale); + router.push(target); } - - triggerSilentReload(queryClient); - const target = localizePath(exitHref || "/questions-list", locale); - router.push(target); }, [exitHref, flushAnswers, locale, router, isSubmitting, queryClient]); const markOptionalQuestionsPassed = useCallback( diff --git a/src/components/Componentes/slider-page.test.tsx b/src/components/Componentes/slider-page.test.tsx new file mode 100644 index 0000000..8b8b317 --- /dev/null +++ b/src/components/Componentes/slider-page.test.tsx @@ -0,0 +1,265 @@ +import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import SliderPage from './slider-page'; + +const mockMutateAsync = vi.fn(); +const mockGetMarriageProfile = vi.fn(); +const mockSetQueryData = vi.fn(); +const mockReplace = vi.fn(); +const mockRemoveItem = vi.spyOn(Storage.prototype, 'removeItem'); +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +vi.mock('@/hooks/marriage/use-profile-basic', () => ({ + useUpdateMarriageProfileBasicMutation: () => ({ + mutateAsync: mockMutateAsync, + isPending: false, + }), +})); + +vi.mock('@/hooks/marriage/use-profile-main', () => ({ + getMarriageProfile: (...args: any[]) => mockGetMarriageProfile(...args), + useMarriageProfileQuery: () => ({ data: undefined, refetch: vi.fn() }), +})); + +vi.mock('@/hooks/marriage/query-keys', () => ({ + marriageQueryKeys: { + profile: () => ['marriage', 'profile'], + }, +})); + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + replace: mockReplace, + back: vi.fn(), + }), +})); + +vi.mock('@/translations/provider', () => ({ + useI18n: () => ({ + locale: 'en', + dictionary: { + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again.", + "Accept & Continue": "Accept & Continue", + }, + }), +})); + +vi.mock('@/translations/config', () => ({ + localizePath: (path: string, locale: string) => path, +})); + +describe('SliderPage', () => { + let queryClient: QueryClient; + + beforeEach(() => { + vi.resetAllMocks(); + queryClient = new QueryClient(); + vi.spyOn(queryClient, 'setQueryData'); + }); + + afterEach(() => { + vi.useRealTimers(); + cleanup(); + }); + + const navigateToFinalSlide = async () => { + render( + + + + ); + const dots = screen.getAllByRole('button', { name: /go to slide/i }); + fireEvent.click(dots[4]); // Go directly to slide 5 + return await screen.findByText('Finish'); + }; + + it('a. Success: PATCH ok, GET ok -> replace', async () => { + const finishBtn = await navigateToFinalSlide(); + const initialRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; + + let resolveGet: any; + + mockMutateAsync.mockResolvedValueOnce({ + status: 'pending_info', + gender: 'female', + is_registering_for_self: true + }); + + mockGetMarriageProfile.mockReturnValueOnce(new Promise(resolve => { + resolveGet = resolve; + })); + + fireEvent.click(finishBtn); + + // Wait for PATCH to resolve and GET to be called + await waitFor(() => { + expect(mockMutateAsync).toHaveBeenCalled(); + expect(mockGetMarriageProfile).toHaveBeenCalled(); + }); + + // Assert navigation and localStorage clear did not happen before GET resolves + expect(mockReplace).not.toHaveBeenCalled(); + const currentRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; + expect(currentRemoveCount).toBe(initialRemoveCount); + + // Resolve GET + resolveGet({ + status: 'pending_info', + gender: 'female', + is_registering_for_self: true + }); + + await waitFor(() => { + expect(queryClient.setQueryData).toHaveBeenCalled(); + const finalRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; + expect(finalRemoveCount).toBe(initialRemoveCount + 1); + expect(mockReplace).toHaveBeenCalledWith('/questions-list/personal_info'); + }); + }); + + it('b. PATCH failure: no nav, Error UI, Retry works', async () => { + const finishBtn = await navigateToFinalSlide(); + + const initialRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; + + mockMutateAsync.mockRejectedValueOnce(new Error('Network Error')); + + fireEvent.click(finishBtn); + + await waitFor(() => { + expect(screen.getByRole('alert')).toHaveTextContent('Failed to update profile basic details. Please try again.'); + }); + expect(mockReplace).not.toHaveBeenCalled(); + + const currentRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; + expect(currentRemoveCount).toBe(initialRemoveCount); + + // Retry succeeds + mockMutateAsync.mockResolvedValueOnce({ + status: 'pending_info', + gender: 'female', + is_registering_for_self: true + }); + mockGetMarriageProfile.mockResolvedValueOnce({ + status: 'pending_info', + gender: 'female', + is_registering_for_self: true + }); + + const retryBtn = screen.getByText('Finish'); + fireEvent.click(retryBtn); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/questions-list/personal_info'); + }); + + const finalRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; + expect(finalRemoveCount).toBe(initialRemoveCount + 1); + }); + + it('c. Slow request: Promise pending -> no redirect', async () => { + const finishBtn = await navigateToFinalSlide(); + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date'] }); + + let resolvePatch: any; + mockMutateAsync.mockReturnValueOnce(new Promise(resolve => { + resolvePatch = resolve; + })); + mockGetMarriageProfile.mockResolvedValueOnce({ + status: 'pending_info', + gender: 'female', + is_registering_for_self: true + }); + + fireEvent.click(finishBtn); + + await vi.advanceTimersByTimeAsync(6000); + + expect(mockReplace).not.toHaveBeenCalled(); + + resolvePatch({ + status: 'pending_info', + gender: 'female', + is_registering_for_self: true + }); + + vi.useRealTimers(); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/questions-list/personal_info'); + }); + }); + + it('d. Double-check failure: GET returns pending_onboarding -> Error UI', async () => { + const finishBtn = await navigateToFinalSlide(); + + mockMutateAsync.mockResolvedValueOnce({ + status: 'pending_info', + gender: 'female', + is_registering_for_self: true + }); + + mockGetMarriageProfile.mockResolvedValueOnce({ + status: 'pending_onboarding', + gender: 'female', + is_registering_for_self: true + }); + + fireEvent.click(finishBtn); + + await waitFor(() => { + expect(screen.getByRole('alert')).toHaveTextContent('Failed to update profile basic details. Please try again.'); + }); + expect(mockReplace).not.toHaveBeenCalled(); + }); + + it('e. Double click: only 1 request', async () => { + const finishBtn = await navigateToFinalSlide(); + + let resolvePatch: any; + mockMutateAsync.mockReturnValueOnce(new Promise(resolve => { + resolvePatch = resolve; + })); + mockGetMarriageProfile.mockResolvedValueOnce({ + status: 'pending_info', + gender: 'female', + is_registering_for_self: true + }); + + fireEvent.click(finishBtn); + fireEvent.click(finishBtn); + fireEvent.click(finishBtn); + + expect(mockMutateAsync).toHaveBeenCalledTimes(1); + + resolvePatch({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalled(); + }); + }); + + it('f. Double-check failure: GET returns waiting -> Error UI', async () => { + const finishBtn = await navigateToFinalSlide(); + + mockMutateAsync.mockResolvedValueOnce({ + status: 'pending_info', + gender: 'female', + is_registering_for_self: true + }); + + mockGetMarriageProfile.mockResolvedValueOnce({ + status: 'waiting', + gender: 'female', + is_registering_for_self: true + }); + + fireEvent.click(finishBtn); + + await waitFor(() => { + expect(screen.getByRole('alert')).toHaveTextContent('Failed to update profile basic details. Please try again.'); + }); + expect(mockReplace).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/Componentes/slider-page.tsx b/src/components/Componentes/slider-page.tsx index 90272e7..20bd1c6 100644 --- a/src/components/Componentes/slider-page.tsx +++ b/src/components/Componentes/slider-page.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/navigation"; import type { TouchEvent } from "react"; import { useEffect, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; import { useUpdateMarriageProfileBasicMutation } from "@/hooks/marriage/use-profile-basic"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; @@ -29,6 +30,10 @@ export default function SliderPage() { const [hasReadRules, setHasReadRules] = useState(false); const [touchStartX, setTouchStartX] = useState(null); const updateProfileBasicMutation = useUpdateMarriageProfileBasicMutation(); + const queryClient = useQueryClient(); + const [submitError, setSubmitError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const { dictionary: t } = useI18n(); const hasFinalNotice = true; const maxSlideIndex = FINAL_SLIDE_COUNT - 1; const displaySlideCount = FINAL_SLIDE_COUNT; @@ -54,36 +59,51 @@ export default function SliderPage() { }; const completeSlider = async () => { - const navigateAway = () => { - if (typeof window !== "undefined") { - window.localStorage.removeItem(SLIDER_ANSWERS_STORAGE_KEY); - // Use hard navigation instead of router.push() to ensure the - // destination page loads with a fresh auth state. SPA (soft) - // navigation sometimes leaves stale query-cache / token state - // that causes the profile query to return 401 and the page to - // stay stuck on the loading spinner. - window.location.href = localizePath( - "/questions-list/personal_info", - locale, - ); - return; - } - router.push(localizePath("/questions-list/personal_info", locale)); - }; + if (isSubmitting) return; + setIsSubmitting(true); + setSubmitError(null); - // Safety timeout — navigate after 5 seconds even if the request hangs - const timeout = setTimeout(navigateAway, 5000); + const genderPayload = selectedGender === "man" ? "male" : "female"; + const isRegisteringPayload = selectedRegistration === "self"; try { - await updateProfileBasicMutation.mutateAsync({ - gender: selectedGender === "man" ? "male" : "female", - is_registering_for_self: selectedRegistration === "self", + const patchResponse = await updateProfileBasicMutation.mutateAsync({ + gender: genderPayload, + is_registering_for_self: isRegisteringPayload, }); + + if ( + patchResponse.status === "pending_onboarding" || + patchResponse.gender !== genderPayload || + patchResponse.is_registering_for_self !== isRegisteringPayload + ) { + throw new Error("Invalid PATCH response"); + } + + const { getMarriageProfile } = await import("@/hooks/marriage/use-profile-main"); + const freshProfile = await getMarriageProfile(); + + if ( + freshProfile.status !== "pending_info" || + freshProfile.gender !== genderPayload || + freshProfile.is_registering_for_self !== isRegisteringPayload + ) { + throw new Error("Invalid GET double-check response"); + } + + const { marriageQueryKeys } = await import("@/hooks/marriage/query-keys"); + queryClient.setQueryData(marriageQueryKeys.profile(), freshProfile); + + if (typeof window !== "undefined") { + window.localStorage.removeItem(SLIDER_ANSWERS_STORAGE_KEY); + } + + router.replace(localizePath("/questions-list/personal_info", locale)); } catch (error) { - console.warn("Failed to update profile basic details:", error); + console.error("Failed to complete onboarding:", error); + setSubmitError(t["Failed to update profile basic details. Please try again."] || "Failed to update profile basic details. Please try again."); } finally { - clearTimeout(timeout); - navigateAway(); + setIsSubmitting(false); } }; @@ -184,11 +204,18 @@ export default function SliderPage() { disabled={!hasReadRules} /> ) : activeSlide === maxSlideIndex ? ( - +
+ {submitError && ( +
+ {submitError} +
+ )} + +
) : ( ({ + isFirstEntryCompleted: vi.fn(), +})); + +vi.mock('./match-start-grace', () => ({ + clearLegacyMatchSubmittedFlag: vi.fn(), + isWithinMatchStartGrace: vi.fn(), +})); + +describe('getSubmitPath', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(firstEntryHelper.isFirstEntryCompleted).mockReturnValue(true); + vi.mocked(matchStartGrace.isWithinMatchStartGrace).mockReturnValue(false); + }); + + const baseProfile: MarriageProfileResponse = { + status: 'pending_onboarding', + id: 1, + gender: 'male', + is_registering_for_self: true, + }; + + it('pending_onboarding returns /terms', () => { + expect(getSubmitPath({ ...baseProfile, status: 'pending_onboarding' })).toBe('/terms'); + }); + + it('waiting returns /finding-match', () => { + expect(getSubmitPath({ ...baseProfile, status: 'waiting' })).toBe('/finding-match'); + }); + + it('pending_info returns questions path', () => { + expect(getSubmitPath({ ...baseProfile, status: 'pending_info' })).toBe('/questions-list'); + + vi.mocked(firstEntryHelper.isFirstEntryCompleted).mockReturnValue(false); + expect(getSubmitPath({ ...baseProfile, status: 'pending_info' })).toBe('/questions-list/personal_info'); + }); + + it('matched returns /request-accepted', () => { + expect(getSubmitPath({ ...baseProfile, status: 'matched' })).toBe('/request-accepted'); + }); + + describe('active_case priority', () => { + it('returns /new-match when status="pending_info" but active_case is introduced and action pending', () => { + const profileWithCase = { + ...baseProfile, + status: 'pending_info' as const, + active_case: { + status: 'introduced' as const, + my_action: 'pending' as const, + }, + }; + // Type assertion added because the mock profile object is lacking many properties of the full MarriageProfileResponse, + // but it contains everything needed for the logic being tested. + expect(getSubmitPath(profileWithCase as any)).toBe('/new-match'); + }); + + it('returns /request-accepted when status="pending_info" but active_case is payment_done', () => { + const profileWithCase = { + ...baseProfile, + status: 'pending_info' as const, + active_case: { + status: 'payment_done' as const, + my_action: 'pending' as const, + }, + }; + expect(getSubmitPath(profileWithCase as any)).toBe('/request-accepted'); + }); + + it('returns /request-sent when status="waiting" but active_case is male_accepted and action done', () => { + const profileWithCase = { + ...baseProfile, + status: 'waiting' as const, + active_case: { + status: 'male_accepted' as const, + my_action: 'done' as const, + }, + }; + expect(getSubmitPath(profileWithCase as any)).toBe('/request-sent'); + }); + }); +}); diff --git a/src/translations/locales/ar.json b/src/translations/locales/ar.json index 9247879..3df25fe 100644 --- a/src/translations/locales/ar.json +++ b/src/translations/locales/ar.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/az.json b/src/translations/locales/az.json index 7477ebc..eec7820 100644 --- a/src/translations/locales/az.json +++ b/src/translations/locales/az.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/bn.json b/src/translations/locales/bn.json index 5ea9ca7..0f542d6 100644 --- a/src/translations/locales/bn.json +++ b/src/translations/locales/bn.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/da.json b/src/translations/locales/da.json index dffe6c6..4a4c735 100644 --- a/src/translations/locales/da.json +++ b/src/translations/locales/da.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/de.json b/src/translations/locales/de.json index 74ba686..89d1cd7 100644 --- a/src/translations/locales/de.json +++ b/src/translations/locales/de.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index 099ee24..bdf19a1 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -760,7 +760,6 @@ "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ This section is completely confidential and is only used for matching and review by advisors.", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Marriage advisors": "Marriage advisors", - "Dr. Hasti Masoudi": "Dr. Hasti Masoudi", "Specialist in clinical psychology": "Specialist in clinical psychology", "Personal Development": "Personal Development", "Family Counselor": "Family Counselor", @@ -804,5 +803,6 @@ "Improving Mutual Understanding": "Improving Mutual Understanding", "Helps individuals better understand their own needs as well as their partner’s motivations, preferences, and emotional priorities.": "Helps individuals better understand their own needs as well as their partner’s motivations, preferences, and emotional priorities.", "Supporting More Suitable Match Recommendations": "Supporting More Suitable Match Recommendations", - "Combining needs and personality assessments with interviews and other marriage criteria can help make partner recommendations more personalized and well-matched.": "Combining needs and personality assessments with interviews and other marriage criteria can help make partner recommendations more personalized and well-matched." -} + "Combining needs and personality assessments with interviews and other marriage criteria can help make partner recommendations more personalized and well-matched.": "Combining needs and personality assessments with interviews and other marriage criteria can help make partner recommendations more personalized and well-matched.", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/es.json b/src/translations/locales/es.json index c163f8a..806829d 100644 --- a/src/translations/locales/es.json +++ b/src/translations/locales/es.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json index 3148936..4c332f3 100644 --- a/src/translations/locales/fa.json +++ b/src/translations/locales/fa.json @@ -760,7 +760,6 @@ "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ این بخش کاملاً محرمانه است و فقط برای مچینگ و بررسی کارشناسان استفاده میشود.", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Marriage advisors": "مشاوران ازدواج", - "Dr. Hasti Masoudi": "دکتر هستی مسعودی", "Specialist in clinical psychology": "متخصص روانشناسی بالینی", "Personal Development": "رشد فردی", "Family Counselor": "مشاور خانواده", @@ -804,5 +803,6 @@ "Improving Mutual Understanding": "بهبود درک متقابل", "Helps individuals better understand their own needs as well as their partner’s motivations, preferences, and emotional priorities.": "به افراد کمک می‌کند تا نیازهای خود و همچنین انگیزه‌ها، ترجیحات و اولویت‌های عاطفی شریک زندگی خود را بهتر درک کنند.", "Supporting More Suitable Match Recommendations": "پشتیبانی از پیشنهادهای همسریابی مناسب‌تر", - "Combining needs and personality assessments with interviews and other marriage criteria can help make partner recommendations more personalized and well-matched.": "ترکیب ارزیابی نیازها و شخصیت با مصاحبه‌ها و سایر معیارهای ازدواج می‌تواند به شخصی‌سازی و هماهنگی بیشتر پیشنهادهای شریک زندگی کمک کند." -} + "Combining needs and personality assessments with interviews and other marriage criteria can help make partner recommendations more personalized and well-matched.": "ترکیب ارزیابی نیازها و شخصیت با مصاحبه‌ها و سایر معیارهای ازدواج می‌تواند به شخصی‌سازی و هماهنگی بیشتر پیشنهادهای شریک زندگی کمک کند.", + "Failed to update profile basic details. Please try again.": "به‌روزرسانی اطلاعات اولیه پروفایل ناموفق بود. لطفاً دوباره تلاش کنید." +} \ No newline at end of file diff --git a/src/translations/locales/fr.json b/src/translations/locales/fr.json index 4cb027f..aefba62 100644 --- a/src/translations/locales/fr.json +++ b/src/translations/locales/fr.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/gu.json b/src/translations/locales/gu.json index 3fc63f8..926c717 100644 --- a/src/translations/locales/gu.json +++ b/src/translations/locales/gu.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/ha.json b/src/translations/locales/ha.json index 70f65d1..d30d484 100644 --- a/src/translations/locales/ha.json +++ b/src/translations/locales/ha.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/he.json b/src/translations/locales/he.json index 8f8ee63..2936a73 100644 --- a/src/translations/locales/he.json +++ b/src/translations/locales/he.json @@ -279,5 +279,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/hi.json b/src/translations/locales/hi.json index 6ad9589..3edfd52 100644 --- a/src/translations/locales/hi.json +++ b/src/translations/locales/hi.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json index 207c50e..4ba469f 100644 --- a/src/translations/locales/id.json +++ b/src/translations/locales/id.json @@ -279,5 +279,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/ks.json b/src/translations/locales/ks.json index 0b4a21f..4200af9 100644 --- a/src/translations/locales/ks.json +++ b/src/translations/locales/ks.json @@ -279,5 +279,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/pt.json b/src/translations/locales/pt.json index fea243e..7ffac86 100644 --- a/src/translations/locales/pt.json +++ b/src/translations/locales/pt.json @@ -279,5 +279,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/ru.json b/src/translations/locales/ru.json index 913f89e..6f4200a 100644 --- a/src/translations/locales/ru.json +++ b/src/translations/locales/ru.json @@ -758,5 +758,6 @@ "{completed} of {total} required steps completed": "Выполнено {completed} из {total} требуемых шагов", "{days} days remaining of your subscription.": "{days} дней до конца вашей подписки.", "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ Этот раздел полностью конфиденциальен и используется только консультантами для сопоставления и проверки.", - "Please select the reason for cancellation:": "Пожалуйста, выберите причину отмены:" -} + "Please select the reason for cancellation:": "Пожалуйста, выберите причину отмены:", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/sw.json b/src/translations/locales/sw.json index 58cf9ed..63c428f 100644 --- a/src/translations/locales/sw.json +++ b/src/translations/locales/sw.json @@ -279,5 +279,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/tg.json b/src/translations/locales/tg.json index 8106b95..26ef8af 100644 --- a/src/translations/locales/tg.json +++ b/src/translations/locales/tg.json @@ -279,5 +279,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/tr.json b/src/translations/locales/tr.json index fc7a13c..0f1fcd3 100644 --- a/src/translations/locales/tr.json +++ b/src/translations/locales/tr.json @@ -279,5 +279,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/ul.json b/src/translations/locales/ul.json index 9aaa91c..5e5901b 100644 --- a/src/translations/locales/ul.json +++ b/src/translations/locales/ul.json @@ -279,5 +279,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/ur.json b/src/translations/locales/ur.json index 4739779..c088723 100644 --- a/src/translations/locales/ur.json +++ b/src/translations/locales/ur.json @@ -279,5 +279,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/uz.json b/src/translations/locales/uz.json index 29aa189..bcc1543 100644 --- a/src/translations/locales/uz.json +++ b/src/translations/locales/uz.json @@ -279,5 +279,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file diff --git a/src/translations/locales/zh.json b/src/translations/locales/zh.json index 81e6abc..d117526 100644 --- a/src/translations/locales/zh.json +++ b/src/translations/locales/zh.json @@ -753,5 +753,6 @@ "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "Please select the reason for cancellation:": "Please select the reason for cancellation:", "Swipe to continue": "Swipe to continue", - "Swipe to confirm cancellation": "Swipe to confirm cancellation" -} + "Swipe to confirm cancellation": "Swipe to confirm cancellation", + "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again." +} \ No newline at end of file