From 5aa5cdf092f8c94f743e0c053530add336ca8c99 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Tue, 10 Feb 2026 20:42:34 +0100 Subject: [PATCH 01/17] feat: Migrate regions to V2 API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add V2 regions API client (fetchRegions, fetchRegionByCode) - Add RegionsAdapter to convert V2 API response to frontend format - Update useRegions hook to fetch from V2 API using React Query - Remove year parameter from useRegions/useRegionsList (API handles versioning) - Update all call sites to use new single-argument signature - Remove unused CURRENT_YEAR imports from updated files - Add regionKeys to queryKeys for React Query cache management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/src/adapters/RegionsAdapter.ts | 34 +++++++ app/src/adapters/index.ts | 1 + app/src/api/v2/index.ts | 3 + app/src/api/v2/regions.ts | 73 +++++++++++++++ app/src/hooks/useRegions.ts | 90 ++++++++++++++----- app/src/hooks/useUserGeographic.ts | 4 +- app/src/hooks/useUserReports.ts | 7 +- app/src/hooks/useUserSimulations.ts | 4 +- .../hooks/utils/useFetchReportIngredients.ts | 4 +- app/src/libs/queryKeys.ts | 7 ++ app/src/pages/Populations.page.tsx | 5 +- .../BudgetaryImpactByProgramSubPage.tsx | 4 +- .../BudgetaryImpactSubPage.tsx | 4 +- .../AbsoluteChangeByDistrict.tsx | 4 +- .../RelativeChangeByDistrict.tsx | 4 +- ...stributionalImpactIncomeAverageSubPage.tsx | 4 +- ...tributionalImpactIncomeRelativeSubPage.tsx | 4 +- ...stributionalImpactWealthAverageSubPage.tsx | 4 +- ...tributionalImpactWealthRelativeSubPage.tsx | 4 +- .../WinnersLosersIncomeDecileSubPage.tsx | 4 +- .../WinnersLosersWealthDecileSubPage.tsx | 4 +- .../InequalityImpactSubPage.tsx | 4 +- .../DeepPovertyImpactByAgeSubPage.tsx | 4 +- .../DeepPovertyImpactByGenderSubPage.tsx | 4 +- .../PovertyImpactByAgeSubPage.tsx | 4 +- .../PovertyImpactByGenderSubPage.tsx | 4 +- .../PovertyImpactByRaceSubPage.tsx | 4 +- .../population/PopulationPathwayWrapper.tsx | 3 +- .../pathways/report/ReportPathwayWrapper.tsx | 3 +- .../population/PopulationExistingView.tsx | 5 +- .../simulation/SimulationPathwayWrapper.tsx | 5 +- 31 files changed, 214 insertions(+), 98 deletions(-) create mode 100644 app/src/adapters/RegionsAdapter.ts create mode 100644 app/src/api/v2/regions.ts diff --git a/app/src/adapters/RegionsAdapter.ts b/app/src/adapters/RegionsAdapter.ts new file mode 100644 index 000000000..4c60e7338 --- /dev/null +++ b/app/src/adapters/RegionsAdapter.ts @@ -0,0 +1,34 @@ +import { V2RegionMetadata } from '@/api/v2'; +import { MetadataRegionEntry } from '@/types/metadata'; + +/** + * Adapter for converting between V2 API region data and internal formats + */ +export class RegionsAdapter { + /** + * Convert a single V2 region to frontend MetadataRegionEntry + * + * Maps API fields to the existing frontend region structure: + * - code -> name (the unique identifier) + * - label -> label (display name) + * - region_type -> type + * - state_code -> state_abbreviation + * - state_name -> state_name + */ + static regionFromV2(region: V2RegionMetadata): MetadataRegionEntry { + return { + name: region.code, + label: region.label, + type: region.region_type as MetadataRegionEntry['type'], + state_abbreviation: region.state_code ?? undefined, + state_name: region.state_name ?? undefined, + }; + } + + /** + * Convert V2 regions array to frontend MetadataRegionEntry array + */ + static regionsFromV2(regions: V2RegionMetadata[]): MetadataRegionEntry[] { + return regions.map((r) => RegionsAdapter.regionFromV2(r)); + } +} diff --git a/app/src/adapters/index.ts b/app/src/adapters/index.ts index b6c1554cc..be7779f5c 100644 --- a/app/src/adapters/index.ts +++ b/app/src/adapters/index.ts @@ -5,6 +5,7 @@ export { ReportAdapter } from './ReportAdapter'; export { HouseholdAdapter } from './HouseholdAdapter'; export { MetadataAdapter } from './MetadataAdapter'; export type { DatasetEntry } from './MetadataAdapter'; +export { RegionsAdapter } from './RegionsAdapter'; // User Ingredient Adapters export { UserReportAdapter } from './UserReportAdapter'; diff --git a/app/src/api/v2/index.ts b/app/src/api/v2/index.ts index 14b009cf8..2b38aeaf9 100644 --- a/app/src/api/v2/index.ts +++ b/app/src/api/v2/index.ts @@ -21,3 +21,6 @@ export { fetchParameterValues, BASELINE_POLICY_ID } from './parameterValues'; // Datasets export { fetchDatasets } from './datasets'; + +// Regions +export { fetchRegions, fetchRegionByCode, type V2RegionMetadata } from './regions'; diff --git a/app/src/api/v2/regions.ts b/app/src/api/v2/regions.ts new file mode 100644 index 000000000..90d64d734 --- /dev/null +++ b/app/src/api/v2/regions.ts @@ -0,0 +1,73 @@ +import { API_V2_BASE_URL, getModelName } from './taxBenefitModels'; + +/** + * V2 API Region response type + * Matches the RegionRead schema from the API + */ +export interface V2RegionMetadata { + id: string; + code: string; // e.g., "state/ca", "constituency/Sheffield Central" + label: string; // e.g., "California", "Sheffield Central" + region_type: string; // e.g., "state", "congressional_district", "constituency" + requires_filter: boolean; + filter_field: string | null; // e.g., "state_code", "place_fips" + filter_value: string | null; // e.g., "CA", "44000" + parent_code: string | null; // e.g., "us", "state/ca" + state_code: string | null; // For US regions + state_name: string | null; // For US regions + dataset_id: string; + tax_benefit_model_id: string; + created_at: string; + updated_at: string; +} + +/** + * Fetch all regions for a country + * + * @param countryId - Country ID (e.g., 'us', 'uk') + * @param regionType - Optional region type filter (e.g., 'state', 'congressional_district') + */ +export async function fetchRegions( + countryId: string, + regionType?: string +): Promise { + const modelName = getModelName(countryId); + let url = `${API_V2_BASE_URL}/regions/?tax_benefit_model_name=${modelName}`; + + if (regionType) { + url += `®ion_type=${encodeURIComponent(regionType)}`; + } + + const res = await fetch(url); + + if (!res.ok) { + throw new Error(`Failed to fetch regions for ${countryId}`); + } + + return res.json(); +} + +/** + * Fetch a specific region by code + * + * @param countryId - Country ID (e.g., 'us', 'uk') + * @param regionCode - Region code (e.g., 'state/ca', 'us') + */ +export async function fetchRegionByCode( + countryId: string, + regionCode: string +): Promise { + const modelName = getModelName(countryId); + const url = `${API_V2_BASE_URL}/regions/by-code/${encodeURIComponent(regionCode)}?tax_benefit_model_name=${modelName}`; + + const res = await fetch(url); + + if (!res.ok) { + if (res.status === 404) { + throw new Error(`Region not found: ${regionCode}`); + } + throw new Error(`Failed to fetch region ${regionCode} for ${countryId}`); + } + + return res.json(); +} diff --git a/app/src/hooks/useRegions.ts b/app/src/hooks/useRegions.ts index 578439093..394653127 100644 --- a/app/src/hooks/useRegions.ts +++ b/app/src/hooks/useRegions.ts @@ -1,51 +1,95 @@ /** - * Hook for accessing regions based on country and simulation year + * Hook for accessing regions from the V2 API * - * Regions are derived data, not stored state. This hook computes the correct - * set of regions based on the country and simulation year, supporting multiple - * versions of dynamic regions (congressional districts, constituencies, etc.) + * Regions are fetched from the API based on country. This replaces the + * previous static data approach with dynamic API-driven region data. */ -import { useMemo } from 'react'; -import { ResolvedRegions, resolveRegions } from '@/data/static/regions'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { RegionsAdapter } from '@/adapters'; +import { fetchRegions, V2RegionMetadata } from '@/api/v2'; +import { regionKeys } from '@/libs/queryKeys'; +import { MetadataRegionEntry } from '@/types/metadata'; /** - * Get regions for a country and simulation year + * Result type for useRegions hook + */ +export interface RegionsResult { + regions: MetadataRegionEntry[]; + isLoading: boolean; + error: Error | null; + /** + * Raw V2 API region data for when you need filter_field, filter_value, etc. + */ + rawRegions: V2RegionMetadata[]; +} + +/** + * Get regions for a country from the V2 API * - * This hook returns the appropriate set of regions based on: - * - countryId: 'us' or 'uk' - * - year: The simulation year (determines which version of dynamic regions to use) + * This hook fetches and returns all regions for a country. + * Regions include states, cities, congressional districts, constituencies, etc. * - * The returned regions include both static regions (states, countries) and - * dynamic regions (congressional districts, constituencies, local authorities) - * resolved to the correct version for the given year. + * @param countryId - Country to fetch regions for (e.g., 'us', 'uk') * * @example * ```tsx * function PopulationScopeView() { * const countryId = useCurrentCountry(); - * const simulationYear = useSelector(selectSimulationYear); + * const { regions, isLoading, error } = useRegions(countryId); * - * const { regions, versions } = useRegions(countryId, simulationYear); + * if (isLoading) return ; + * if (error) return ; * * // Filter for specific region types - * const constituencies = getUKConstituencies(regions); - * const districts = getUSCongressionalDistricts(regions); + * const states = regions.filter(r => r.type === 'state'); + * const districts = regions.filter(r => r.type === 'congressional_district'); * * return ; * } * ``` */ -export function useRegions(countryId: string, year: number): ResolvedRegions { - return useMemo(() => resolveRegions(countryId, year), [countryId, year]); +export function useRegions(countryId: string): RegionsResult { + const query: UseQueryResult = useQuery({ + queryKey: regionKeys.byCountry(countryId), + queryFn: () => fetchRegions(countryId), + enabled: !!countryId, + staleTime: 5 * 60 * 1000, // 5 minutes - regions don't change often + }); + + return { + regions: query.data ? RegionsAdapter.regionsFromV2(query.data) : [], + isLoading: query.isLoading, + error: query.error, + rawRegions: query.data ?? [], + }; } /** - * Get just the regions array for a country and year + * Get just the regions array for a country * - * Convenience wrapper when you don't need version information. + * Convenience wrapper when you don't need loading/error state. */ -export function useRegionsList(countryId: string, year: number): ResolvedRegions['regions'] { - const { regions } = useRegions(countryId, year); +export function useRegionsList(countryId: string): MetadataRegionEntry[] { + const { regions } = useRegions(countryId); return regions; } + +/** + * Get a specific region by code + * + * @param countryId - Country ID (e.g., 'us', 'uk') + * @param regionCode - Region code (e.g., 'state/ca', 'us') + */ +export function useRegionByCode( + countryId: string, + regionCode: string | undefined +): V2RegionMetadata | undefined { + const { rawRegions } = useRegions(countryId); + + if (!regionCode) { + return undefined; + } + + return rawRegions.find((r) => r.code === regionCode); +} diff --git a/app/src/hooks/useUserGeographic.ts b/app/src/hooks/useUserGeographic.ts index 1b387aaee..1a7736db9 100644 --- a/app/src/hooks/useUserGeographic.ts +++ b/app/src/hooks/useUserGeographic.ts @@ -1,6 +1,5 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ApiGeographicStore, LocalStorageGeographicStore } from '@/api/geographicAssociation'; -import { CURRENT_YEAR } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { useRegionsList } from '@/hooks/useStaticMetadata'; import { queryConfig } from '@/libs/queryConfig'; @@ -133,8 +132,7 @@ export function isGeographicMetadataWithAssociation( export const useUserGeographics = (userId: string) => { // Get regions from static metadata for label lookups const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); // First, get the populations const { diff --git a/app/src/hooks/useUserReports.ts b/app/src/hooks/useUserReports.ts index 74e48f804..412246aa7 100644 --- a/app/src/hooks/useUserReports.ts +++ b/app/src/hooks/useUserReports.ts @@ -5,7 +5,6 @@ import { fetchHouseholdById } from '@/api/household'; import { fetchPolicyById } from '@/api/policy'; import { fetchReportById } from '@/api/report'; import { fetchSimulationById } from '@/api/simulation'; -import { CURRENT_YEAR } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { Geography } from '@/types/ingredients/Geography'; import { Household } from '@/types/ingredients/Household'; @@ -76,8 +75,7 @@ export const useUserReports = (userId: string) => { const queryNormalizer = useQueryNormalizer(); // Get geography data from static metadata - const currentYear = parseInt(CURRENT_YEAR, 10); - const geographyOptions = useRegionsList(country, currentYear); + const geographyOptions = useRegionsList(country); // Step 1: Fetch all user associations in parallel const { @@ -347,7 +345,6 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo const queryNormalizer = useQueryNormalizer(); const country = useCurrentCountry(); const isEnabled = options?.enabled !== false; - const currentYear = parseInt(CURRENT_YEAR, 10); // Step 1: Fetch UserReport by userReportId to get the base reportId const { @@ -460,7 +457,7 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo ); // Step 7: Get geography data from simulations - const geographyOptions = useRegionsList(country, currentYear); + const geographyOptions = useRegionsList(country); const geographies: Geography[] = []; simulations.forEach((sim) => { diff --git a/app/src/hooks/useUserSimulations.ts b/app/src/hooks/useUserSimulations.ts index 92ba00e63..1ffe8932c 100644 --- a/app/src/hooks/useUserSimulations.ts +++ b/app/src/hooks/useUserSimulations.ts @@ -4,7 +4,6 @@ import { HouseholdAdapter, PolicyAdapter, SimulationAdapter } from '@/adapters'; import { fetchHouseholdById } from '@/api/household'; import { fetchPolicyById } from '@/api/policy'; import { fetchSimulationById } from '@/api/simulation'; -import { CURRENT_YEAR } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { Geography } from '@/types/ingredients/Geography'; import { Household } from '@/types/ingredients/Household'; @@ -63,8 +62,7 @@ export const useUserSimulations = (userId: string) => { const queryNormalizer = useQueryNormalizer(); // Get geography data from static metadata - const currentYear = parseInt(CURRENT_YEAR, 10); - const geographyOptions = useRegionsList(country, currentYear); + const geographyOptions = useRegionsList(country); // Step 1: Fetch all user associations in parallel const { diff --git a/app/src/hooks/utils/useFetchReportIngredients.ts b/app/src/hooks/utils/useFetchReportIngredients.ts index 9f5636e4e..27f2d0c63 100644 --- a/app/src/hooks/utils/useFetchReportIngredients.ts +++ b/app/src/hooks/utils/useFetchReportIngredients.ts @@ -16,7 +16,6 @@ import { fetchHouseholdById } from '@/api/household'; import { fetchPolicyById } from '@/api/policy'; import { fetchReportById } from '@/api/report'; import { fetchSimulationById } from '@/api/simulation'; -import { CURRENT_YEAR } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { useRegionsList } from '@/hooks/useStaticMetadata'; import { householdKeys, policyKeys, reportKeys, simulationKeys } from '@/libs/queryKeys'; @@ -187,8 +186,7 @@ export function useFetchReportIngredients( const country = input?.userReport.countryId ?? currentCountry; // Get geography metadata for building Geography objects from static metadata - const currentYear = parseInt(CURRENT_YEAR, 10); - const geographyOptions = useRegionsList(country, currentYear); + const geographyOptions = useRegionsList(country); // Step 1: Fetch the base Report using reportId from userReport const reportId = input?.userReport.reportId; diff --git a/app/src/libs/queryKeys.ts b/app/src/libs/queryKeys.ts index 88f9afbe4..4228ae073 100644 --- a/app/src/libs/queryKeys.ts +++ b/app/src/libs/queryKeys.ts @@ -111,3 +111,10 @@ export const parameterValueKeys = { byPolicyAndParameter: (policyId: string, parameterId: string) => [...parameterValueKeys.all, 'policy', policyId, 'parameter', parameterId] as const, }; + +export const regionKeys = { + all: ['regions'] as const, + byCountry: (countryId: string) => [...regionKeys.all, 'country', countryId] as const, + byCountryAndType: (countryId: string, regionType: string) => + [...regionKeys.all, 'country', countryId, 'type', regionType] as const, +}; diff --git a/app/src/pages/Populations.page.tsx b/app/src/pages/Populations.page.tsx index f518382d2..5e0e0d7a8 100644 --- a/app/src/pages/Populations.page.tsx +++ b/app/src/pages/Populations.page.tsx @@ -5,7 +5,7 @@ import { useDisclosure } from '@mantine/hooks'; import { BulletsValue, ColumnConfig, IngredientRecord, TextValue } from '@/components/columns'; import { RenameIngredientModal } from '@/components/common/RenameIngredientModal'; import IngredientReadView from '@/components/IngredientReadView'; -import { CURRENT_YEAR, MOCK_USER_ID } from '@/constants'; +import { MOCK_USER_ID } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { useRegionsList } from '@/hooks/useStaticMetadata'; import { @@ -23,8 +23,7 @@ export default function PopulationsPage() { const userId = MOCK_USER_ID.toString(); // TODO: Replace with actual user ID retrieval logic // TODO: Session storage hard-fixes "anonymous" as user ID; this should really just be anything const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); // Fetch household associations const { diff --git a/app/src/pages/report-output/budgetary-impact/BudgetaryImpactByProgramSubPage.tsx b/app/src/pages/report-output/budgetary-impact/BudgetaryImpactByProgramSubPage.tsx index 82e51ca21..2519eb18a 100644 --- a/app/src/pages/report-output/budgetary-impact/BudgetaryImpactByProgramSubPage.tsx +++ b/app/src/pages/report-output/budgetary-impact/BudgetaryImpactByProgramSubPage.tsx @@ -5,7 +5,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -29,8 +28,7 @@ interface ProgramBudgetItem { export default function BudgetaryImpactByProgramSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const variables = useSelector((state: RootState) => state.metadata.variables); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/budgetary-impact/BudgetaryImpactSubPage.tsx b/app/src/pages/report-output/budgetary-impact/BudgetaryImpactSubPage.tsx index 90cac2f3e..3e113f5e3 100644 --- a/app/src/pages/report-output/budgetary-impact/BudgetaryImpactSubPage.tsx +++ b/app/src/pages/report-output/budgetary-impact/BudgetaryImpactSubPage.tsx @@ -4,7 +4,6 @@ import { Stack } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -22,8 +21,7 @@ export default function BudgetaryImpactSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const { height: viewportHeight } = useViewportSize(); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const chartHeight = getClampedChartHeight(viewportHeight, mobile); // Extract data diff --git a/app/src/pages/report-output/congressional-district/AbsoluteChangeByDistrict.tsx b/app/src/pages/report-output/congressional-district/AbsoluteChangeByDistrict.tsx index 08c0e5f1f..f312ae615 100644 --- a/app/src/pages/report-output/congressional-district/AbsoluteChangeByDistrict.tsx +++ b/app/src/pages/report-output/congressional-district/AbsoluteChangeByDistrict.tsx @@ -6,7 +6,6 @@ import { } from '@/adapters/congressional-district/congressionalDistrictDataAdapter'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { USDistrictChoroplethMap } from '@/components/visualization/USDistrictChoroplethMap'; -import { CURRENT_YEAR } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { useRegionsList } from '@/hooks/useStaticMetadata'; import type { ReportOutputSocietyWideUS } from '@/types/metadata/ReportOutputSocietyWideUS'; @@ -26,8 +25,7 @@ interface AbsoluteChangeByDistrictProps { export function AbsoluteChangeByDistrict({ output }: AbsoluteChangeByDistrictProps) { // Get district labels from static metadata const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); // Build label lookup from metadata (memoized) const labelLookup = useMemo(() => buildDistrictLabelLookup(regions), [regions]); diff --git a/app/src/pages/report-output/congressional-district/RelativeChangeByDistrict.tsx b/app/src/pages/report-output/congressional-district/RelativeChangeByDistrict.tsx index 63488bc26..f72775ed3 100644 --- a/app/src/pages/report-output/congressional-district/RelativeChangeByDistrict.tsx +++ b/app/src/pages/report-output/congressional-district/RelativeChangeByDistrict.tsx @@ -6,7 +6,6 @@ import { } from '@/adapters/congressional-district/congressionalDistrictDataAdapter'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { USDistrictChoroplethMap } from '@/components/visualization/USDistrictChoroplethMap'; -import { CURRENT_YEAR } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { useRegionsList } from '@/hooks/useStaticMetadata'; import type { ReportOutputSocietyWideUS } from '@/types/metadata/ReportOutputSocietyWideUS'; @@ -26,8 +25,7 @@ interface RelativeChangeByDistrictProps { export function RelativeChangeByDistrict({ output }: RelativeChangeByDistrictProps) { // Get district labels from static metadata const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); // Build label lookup from metadata (memoized) const labelLookup = useMemo(() => buildDistrictLabelLookup(regions), [regions]); diff --git a/app/src/pages/report-output/distributional-impact/DistributionalImpactIncomeAverageSubPage.tsx b/app/src/pages/report-output/distributional-impact/DistributionalImpactIncomeAverageSubPage.tsx index 2601bebda..99f351ae2 100644 --- a/app/src/pages/report-output/distributional-impact/DistributionalImpactIncomeAverageSubPage.tsx +++ b/app/src/pages/report-output/distributional-impact/DistributionalImpactIncomeAverageSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -21,8 +20,7 @@ interface Props { export default function DistributionalImpactIncomeAverageSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/distributional-impact/DistributionalImpactIncomeRelativeSubPage.tsx b/app/src/pages/report-output/distributional-impact/DistributionalImpactIncomeRelativeSubPage.tsx index 37ae86731..af5ff6ed8 100644 --- a/app/src/pages/report-output/distributional-impact/DistributionalImpactIncomeRelativeSubPage.tsx +++ b/app/src/pages/report-output/distributional-impact/DistributionalImpactIncomeRelativeSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -21,8 +20,7 @@ interface Props { export default function DistributionalImpactIncomeRelativeSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/distributional-impact/DistributionalImpactWealthAverageSubPage.tsx b/app/src/pages/report-output/distributional-impact/DistributionalImpactWealthAverageSubPage.tsx index 2febfb40b..5e0d3103b 100644 --- a/app/src/pages/report-output/distributional-impact/DistributionalImpactWealthAverageSubPage.tsx +++ b/app/src/pages/report-output/distributional-impact/DistributionalImpactWealthAverageSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -21,8 +20,7 @@ interface Props { export default function DistributionalImpactWealthAverageSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/distributional-impact/DistributionalImpactWealthRelativeSubPage.tsx b/app/src/pages/report-output/distributional-impact/DistributionalImpactWealthRelativeSubPage.tsx index 4243cdd90..5f249be62 100644 --- a/app/src/pages/report-output/distributional-impact/DistributionalImpactWealthRelativeSubPage.tsx +++ b/app/src/pages/report-output/distributional-impact/DistributionalImpactWealthRelativeSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -21,8 +20,7 @@ interface Props { export default function DistributionalImpactWealthRelativeSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/distributional-impact/WinnersLosersIncomeDecileSubPage.tsx b/app/src/pages/report-output/distributional-impact/WinnersLosersIncomeDecileSubPage.tsx index fa8376169..e96878bbf 100644 --- a/app/src/pages/report-output/distributional-impact/WinnersLosersIncomeDecileSubPage.tsx +++ b/app/src/pages/report-output/distributional-impact/WinnersLosersIncomeDecileSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -58,8 +57,7 @@ const LEGEND_TEXT_MAP: Record = { export default function WinnersLosersIncomeDecileSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/distributional-impact/WinnersLosersWealthDecileSubPage.tsx b/app/src/pages/report-output/distributional-impact/WinnersLosersWealthDecileSubPage.tsx index eeb1ccdbb..183bf2df9 100644 --- a/app/src/pages/report-output/distributional-impact/WinnersLosersWealthDecileSubPage.tsx +++ b/app/src/pages/report-output/distributional-impact/WinnersLosersWealthDecileSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -58,8 +57,7 @@ const LEGEND_TEXT_MAP: Record = { export default function WinnersLosersWealthDecileSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/inequality-impact/InequalityImpactSubPage.tsx b/app/src/pages/report-output/inequality-impact/InequalityImpactSubPage.tsx index 9966a1023..514c7495d 100644 --- a/app/src/pages/report-output/inequality-impact/InequalityImpactSubPage.tsx +++ b/app/src/pages/report-output/inequality-impact/InequalityImpactSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -21,8 +20,7 @@ interface Props { export default function InequalityImpactSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/poverty-impact/DeepPovertyImpactByAgeSubPage.tsx b/app/src/pages/report-output/poverty-impact/DeepPovertyImpactByAgeSubPage.tsx index 7a7e07614..f275e753b 100644 --- a/app/src/pages/report-output/poverty-impact/DeepPovertyImpactByAgeSubPage.tsx +++ b/app/src/pages/report-output/poverty-impact/DeepPovertyImpactByAgeSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -21,8 +20,7 @@ interface Props { export default function DeepPovertyImpactByAgeSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/poverty-impact/DeepPovertyImpactByGenderSubPage.tsx b/app/src/pages/report-output/poverty-impact/DeepPovertyImpactByGenderSubPage.tsx index a6d591d7b..6188217a9 100644 --- a/app/src/pages/report-output/poverty-impact/DeepPovertyImpactByGenderSubPage.tsx +++ b/app/src/pages/report-output/poverty-impact/DeepPovertyImpactByGenderSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -21,8 +20,7 @@ interface Props { export default function DeepPovertyImpactByGenderSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/poverty-impact/PovertyImpactByAgeSubPage.tsx b/app/src/pages/report-output/poverty-impact/PovertyImpactByAgeSubPage.tsx index 1a60b8367..debd3c719 100644 --- a/app/src/pages/report-output/poverty-impact/PovertyImpactByAgeSubPage.tsx +++ b/app/src/pages/report-output/poverty-impact/PovertyImpactByAgeSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -21,8 +20,7 @@ interface Props { export default function PovertyImpactByAgeSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/poverty-impact/PovertyImpactByGenderSubPage.tsx b/app/src/pages/report-output/poverty-impact/PovertyImpactByGenderSubPage.tsx index d1336f599..98e32b0eb 100644 --- a/app/src/pages/report-output/poverty-impact/PovertyImpactByGenderSubPage.tsx +++ b/app/src/pages/report-output/poverty-impact/PovertyImpactByGenderSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -21,8 +20,7 @@ interface Props { export default function PovertyImpactByGenderSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pages/report-output/poverty-impact/PovertyImpactByRaceSubPage.tsx b/app/src/pages/report-output/poverty-impact/PovertyImpactByRaceSubPage.tsx index 10302d8e9..210e5ef8e 100644 --- a/app/src/pages/report-output/poverty-impact/PovertyImpactByRaceSubPage.tsx +++ b/app/src/pages/report-output/poverty-impact/PovertyImpactByRaceSubPage.tsx @@ -4,7 +4,6 @@ import { Stack, Text } from '@mantine/core'; import { useMediaQuery, useViewportSize } from '@mantine/hooks'; import type { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import { ChartContainer } from '@/components/ChartContainer'; -import { CURRENT_YEAR } from '@/constants'; import { colors } from '@/designTokens/colors'; import { spacing } from '@/designTokens/spacing'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; @@ -21,8 +20,7 @@ interface Props { export default function PovertyImpactByRaceSubPage({ output }: Props) { const mobile = useMediaQuery('(max-width: 768px)'); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); const { height: viewportHeight } = useViewportSize(); const chartHeight = getClampedChartHeight(viewportHeight, mobile); diff --git a/app/src/pathways/population/PopulationPathwayWrapper.tsx b/app/src/pathways/population/PopulationPathwayWrapper.tsx index 586dbc938..fbeca151f 100644 --- a/app/src/pathways/population/PopulationPathwayWrapper.tsx +++ b/app/src/pathways/population/PopulationPathwayWrapper.tsx @@ -41,8 +41,7 @@ export default function PopulationPathwayWrapper({ onComplete }: PopulationPathw // Get metadata for views const metadata = useSelector((state: RootState) => state.metadata); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regionData = useRegionsList(countryId, currentYear); + const regionData = useRegionsList(countryId); // ========== NAVIGATION ========== const { currentMode, navigateToMode, goBack, canGoBack } = usePathwayNavigation( diff --git a/app/src/pathways/report/ReportPathwayWrapper.tsx b/app/src/pathways/report/ReportPathwayWrapper.tsx index c104eae78..017e1f05c 100644 --- a/app/src/pathways/report/ReportPathwayWrapper.tsx +++ b/app/src/pathways/report/ReportPathwayWrapper.tsx @@ -92,8 +92,7 @@ export default function ReportPathwayWrapper({ onComplete }: ReportPathwayWrappe // Get metadata for population views const metadata = useSelector((state: RootState) => state.metadata); const currentLawId = useCurrentLawId(countryId); - const reportYear = parseInt(reportState.year || '2025', 10); - const regionData = useRegionsList(countryId, reportYear); + const regionData = useRegionsList(countryId); // ========== NAVIGATION ========== const { currentMode, navigateToMode, goBack, canGoBack } = usePathwayNavigation( diff --git a/app/src/pathways/report/views/population/PopulationExistingView.tsx b/app/src/pathways/report/views/population/PopulationExistingView.tsx index 84eeefda2..653b98448 100644 --- a/app/src/pathways/report/views/population/PopulationExistingView.tsx +++ b/app/src/pathways/report/views/population/PopulationExistingView.tsx @@ -8,7 +8,7 @@ import { useState } from 'react'; import { Text } from '@mantine/core'; import { HouseholdAdapter } from '@/adapters'; import PathwayView from '@/components/common/PathwayView'; -import { CURRENT_YEAR, MOCK_USER_ID } from '@/constants'; +import { MOCK_USER_ID } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { useRegionsList } from '@/hooks/useStaticMetadata'; import { @@ -44,8 +44,7 @@ export default function PopulationExistingView({ }: PopulationExistingViewProps) { const userId = MOCK_USER_ID.toString(); const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); + const regions = useRegionsList(countryId); // Fetch household populations const { diff --git a/app/src/pathways/simulation/SimulationPathwayWrapper.tsx b/app/src/pathways/simulation/SimulationPathwayWrapper.tsx index 5295229dd..8927b4542 100644 --- a/app/src/pathways/simulation/SimulationPathwayWrapper.tsx +++ b/app/src/pathways/simulation/SimulationPathwayWrapper.tsx @@ -9,7 +9,7 @@ import { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import StandardLayout from '@/components/StandardLayout'; -import { CURRENT_YEAR, MOCK_USER_ID } from '@/constants'; +import { MOCK_USER_ID } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { usePathwayNavigation } from '@/hooks/usePathwayNavigation'; import { useCurrentLawId, useRegionsList } from '@/hooks/useStaticMetadata'; @@ -64,8 +64,7 @@ export default function SimulationPathwayWrapper({ onComplete }: SimulationPathw // Get metadata for population views const metadata = useSelector((state: RootState) => state.metadata); const currentLawId = useCurrentLawId(countryId); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regionData = useRegionsList(countryId, currentYear); + const regionData = useRegionsList(countryId); // ========== NAVIGATION ========== const { currentMode, navigateToMode, goBack, canGoBack } = usePathwayNavigation( From e3a29bb7e57d369a16eb9ca9dd09a3d6b4b1beed Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Tue, 10 Feb 2026 23:23:58 +0100 Subject: [PATCH 02/17] chore: Lint --- app/src/hooks/useLazyParameterTree.ts | 2 +- app/src/libs/lazyParameterTree.ts | 5 +- .../components/LazyNestedMenuMocks.ts | 4 +- .../hooks/useLazyParameterTreeMocks.ts | 6 +- .../fixtures/libs/lazyParameterTreeMocks.ts | 2 +- .../components/common/LazyNestedMenu.test.tsx | 86 +++---------------- .../unit/hooks/useLazyParameterTree.test.ts | 5 +- 7 files changed, 22 insertions(+), 88 deletions(-) diff --git a/app/src/hooks/useLazyParameterTree.ts b/app/src/hooks/useLazyParameterTree.ts index 601559c8c..6b02813bd 100644 --- a/app/src/hooks/useLazyParameterTree.ts +++ b/app/src/hooks/useLazyParameterTree.ts @@ -10,9 +10,9 @@ import { useCallback, useRef } from 'react'; import { useSelector } from 'react-redux'; import { + hasChildren as checkHasChildren, createParameterTreeCache, getChildrenForPath, - hasChildren as checkHasChildren, LazyParameterTreeNode, ParameterTreeCache, } from '@/libs/lazyParameterTree'; diff --git a/app/src/libs/lazyParameterTree.ts b/app/src/libs/lazyParameterTree.ts index 4c66e9655..8bae46483 100644 --- a/app/src/libs/lazyParameterTree.ts +++ b/app/src/libs/lazyParameterTree.ts @@ -227,9 +227,6 @@ export function getChildrenForPath( /** * Check if a path has children (without building them). */ -export function hasChildren( - parameters: Record, - path: string -): boolean { +export function hasChildren(parameters: Record, path: string): boolean { return !isLeafParameter(parameters, path); } diff --git a/app/src/tests/fixtures/components/LazyNestedMenuMocks.ts b/app/src/tests/fixtures/components/LazyNestedMenuMocks.ts index 62c7247f5..14e4e0925 100644 --- a/app/src/tests/fixtures/components/LazyNestedMenuMocks.ts +++ b/app/src/tests/fixtures/components/LazyNestedMenuMocks.ts @@ -25,9 +25,7 @@ export const TEST_NODE_LABELS = { } as const; // Factory to create mock menu node -export function createMockNode( - overrides: Partial = {} -): LazyMenuNode { +export function createMockNode(overrides: Partial = {}): LazyMenuNode { return { name: 'gov.test', label: 'Test', diff --git a/app/src/tests/fixtures/hooks/useLazyParameterTreeMocks.ts b/app/src/tests/fixtures/hooks/useLazyParameterTreeMocks.ts index 65becc28e..3b2016017 100644 --- a/app/src/tests/fixtures/hooks/useLazyParameterTreeMocks.ts +++ b/app/src/tests/fixtures/hooks/useLazyParameterTreeMocks.ts @@ -83,9 +83,9 @@ export const MOCK_METADATA_EMPTY: MetadataState = { }; // Factory to create root state -export function createMockRootState( - metadataOverrides: Partial = {} -): { metadata: MetadataState } { +export function createMockRootState(metadataOverrides: Partial = {}): { + metadata: MetadataState; +} { return { metadata: { ...BASE_METADATA_STATE, diff --git a/app/src/tests/fixtures/libs/lazyParameterTreeMocks.ts b/app/src/tests/fixtures/libs/lazyParameterTreeMocks.ts index 7cff7df41..6f39388a7 100644 --- a/app/src/tests/fixtures/libs/lazyParameterTreeMocks.ts +++ b/app/src/tests/fixtures/libs/lazyParameterTreeMocks.ts @@ -1,8 +1,8 @@ /** * Fixtures for lazyParameterTree tests */ -import { ParameterMetadata } from '@/types/metadata'; import { LazyParameterTreeNode, ParameterTreeCache } from '@/libs/lazyParameterTree'; +import { ParameterMetadata } from '@/types/metadata'; // Test paths export const TEST_PATHS = { diff --git a/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx b/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx index 894daf28e..d0ba99221 100644 --- a/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx +++ b/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx @@ -3,8 +3,8 @@ * * Tests the lazy-loading nested menu that fetches children on-demand. */ -import { describe, expect, test, vi, beforeEach } from 'vitest'; import { render, screen, userEvent } from '@test-utils'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import LazyNestedMenu from '@/components/common/LazyNestedMenu'; import { createMockGetChildren, @@ -31,12 +31,7 @@ describe('LazyNestedMenu', () => { const getChildren = createMockGetChildren(); // When - render( - - ); + render(); // Then expect(screen.getByText(TEST_NODE_LABELS.TAX)).toBeInTheDocument(); @@ -48,12 +43,7 @@ describe('LazyNestedMenu', () => { const getChildren = createMockGetChildren(); // When - render( - - ); + render(); // Then - no NavLink items should be rendered expect(screen.queryByRole('link')).not.toBeInTheDocument(); @@ -65,12 +55,7 @@ describe('LazyNestedMenu', () => { const getChildren = createMockGetChildren(); // When - render( - - ); + render(); // Then expect(screen.getByText('Parameter 1')).toBeInTheDocument(); @@ -83,12 +68,7 @@ describe('LazyNestedMenu', () => { // Given const user = userEvent.setup(); const getChildren = createMockGetChildren(); - render( - - ); + render(); // When await user.click(screen.getByText(TEST_NODE_LABELS.TAX)); @@ -104,12 +84,7 @@ describe('LazyNestedMenu', () => { // Given const user = userEvent.setup(); const getChildren = createMockGetChildren(); - render( - - ); + render(); // When - expand then collapse await user.click(screen.getByText(TEST_NODE_LABELS.TAX)); @@ -124,12 +99,7 @@ describe('LazyNestedMenu', () => { // Given const user = userEvent.setup(); const getChildren = createMockGetChildren(); - render( - - ); + render(); // When - expand both await user.click(screen.getByText(TEST_NODE_LABELS.TAX)); @@ -149,12 +119,7 @@ describe('LazyNestedMenu', () => { const getChildren = createMockGetChildren({ [TEST_NODE_NAMES.INCOME_TAX]: grandchildren, }); - render( - - ); + render(); // When - expand tax, then expand income tax await user.click(screen.getByText(TEST_NODE_LABELS.TAX)); @@ -232,12 +197,7 @@ describe('LazyNestedMenu', () => { // Given const user = userEvent.setup(); const getChildren = createMockGetChildren(); - render( - - ); + render(); // When/Then - should not throw await expect(user.click(screen.getByText('Parameter 1'))).resolves.not.toThrow(); @@ -249,12 +209,7 @@ describe('LazyNestedMenu', () => { // Given const user = userEvent.setup(); const getChildren = createMockGetChildren(); - render( - - ); + render(); // When await user.click(screen.getByText(TEST_NODE_LABELS.TAX)); @@ -268,12 +223,7 @@ describe('LazyNestedMenu', () => { // Given const user = userEvent.setup(); const getChildren = createMockGetChildren(); - render( - - ); + render(); // When - click tax, then click benefit await user.click(screen.getByText(TEST_NODE_LABELS.TAX)); @@ -294,12 +244,7 @@ describe('LazyNestedMenu', () => { const getChildren = createMockGetChildren(); // When - render( - - ); + render(); // Then expect(getChildren).not.toHaveBeenCalled(); @@ -309,12 +254,7 @@ describe('LazyNestedMenu', () => { // Given const user = userEvent.setup(); const getChildren = createMockGetChildren(); - render( - - ); + render(); // When - expand, collapse, expand await user.click(screen.getByText(TEST_NODE_LABELS.TAX)); diff --git a/app/src/tests/unit/hooks/useLazyParameterTree.test.ts b/app/src/tests/unit/hooks/useLazyParameterTree.test.ts index 0a3a33145..c71a151b8 100644 --- a/app/src/tests/unit/hooks/useLazyParameterTree.test.ts +++ b/app/src/tests/unit/hooks/useLazyParameterTree.test.ts @@ -3,8 +3,9 @@ * * Tests the React hook that provides on-demand parameter tree access with caching. */ -import { describe, expect, test, vi, beforeEach } from 'vitest'; import { renderHook } from '@test-utils'; +import { useSelector } from 'react-redux'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { useLazyParameterTree } from '@/hooks/useLazyParameterTree'; import { createMockRootState, @@ -27,8 +28,6 @@ vi.mock('react-redux', async () => { }; }); -import { useSelector } from 'react-redux'; - describe('useLazyParameterTree', () => { beforeEach(() => { vi.clearAllMocks(); From 6aa1e75ffa565a1c7bd7f76e5f66340d78e055fb Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Fri, 13 Feb 2026 15:35:52 +0100 Subject: [PATCH 03/17] feat: Remove user associations for geographies --- app/src/adapters/UserGeographicAdapter.ts | 42 -- app/src/adapters/index.ts | 1 - app/src/api/geographicAssociation.ts | 220 -------- app/src/hooks/useSaveSharedReport.ts | 18 +- app/src/hooks/useSharedReportData.ts | 8 +- app/src/hooks/useUserGeographic.ts | 213 -------- app/src/hooks/useUserReports.ts | 14 +- .../hooks/utils/useFetchReportIngredients.ts | 15 +- app/src/libs/queryKeys.ts | 12 - app/src/pages/Populations.page.tsx | 188 +------ app/src/pages/ReportOutput.page.tsx | 7 +- .../pages/report-output/GeographySubPage.tsx | 15 +- .../pages/report-output/PopulationSubPage.tsx | 16 +- .../report-output/SocietyWideReportOutput.tsx | 4 - .../pathways/report/ReportPathwayWrapper.tsx | 6 +- .../pathways/report/views/ReportSetupView.tsx | 8 +- .../views/ReportSimulationSelectionView.tsx | 30 +- .../population/GeographicConfirmationView.tsx | 41 +- .../population/PopulationExistingView.tsx | 206 ++----- .../SimulationPopulationSetupView.tsx | 6 +- .../simulation/SimulationPathwayWrapper.tsx | 7 +- .../tests/fixtures/api/associationFixtures.ts | 19 +- app/src/tests/fixtures/hooks/hooksMocks.ts | 26 +- .../hooks/useFetchReportIngredientsMocks.ts | 27 +- .../hooks/useSaveSharedReportMocks.ts | 11 - .../hooks/useSharedReportDataMocks.tsx | 10 - .../fixtures/hooks/useUserHouseholdMocks.ts | 74 +-- .../tests/fixtures/pages/populationsMocks.ts | 346 ------------ .../fixtures/utils/populationMatchingMocks.ts | 189 ------- .../fixtures/utils/populationOpsMocks.ts | 259 --------- .../tests/fixtures/utils/shareUtilsMocks.ts | 198 ------- .../unit/api/geographicAssociation.test.ts | 479 ----------------- .../unit/hooks/useSharedReportData.test.tsx | 12 +- .../unit/hooks/useUserGeographic.test.tsx | 399 -------------- .../utils/useFetchReportIngredients.test.ts | 16 +- .../unit/pages/Populations.page.test.tsx | 456 ---------------- .../unit/pages/ReportOutput.page.test.tsx | 253 --------- .../report/ReportPathwayWrapper.test.tsx | 165 ------ .../report/views/ReportSetupView.test.tsx | 336 ------------ .../SimulationPathwayWrapper.test.tsx | 143 ----- .../tests/unit/utils/PopulationOps.test.ts | 506 ------------------ .../unit/utils/populationMatching.test.ts | 202 ------- app/src/tests/unit/utils/shareUtils.test.ts | 315 ----------- app/src/types/ingredients/UserPopulation.ts | 17 +- app/src/types/ingredients/index.ts | 14 +- app/src/utils/PopulationOps.ts | 47 +- app/src/utils/populationMatching.ts | 28 +- app/src/utils/shareUtils.ts | 20 +- .../utils/validation/ingredientValidation.ts | 30 -- 49 files changed, 156 insertions(+), 5518 deletions(-) delete mode 100644 app/src/adapters/UserGeographicAdapter.ts delete mode 100644 app/src/api/geographicAssociation.ts delete mode 100644 app/src/hooks/useUserGeographic.ts delete mode 100644 app/src/tests/fixtures/pages/populationsMocks.ts delete mode 100644 app/src/tests/fixtures/utils/populationMatchingMocks.ts delete mode 100644 app/src/tests/fixtures/utils/populationOpsMocks.ts delete mode 100644 app/src/tests/fixtures/utils/shareUtilsMocks.ts delete mode 100644 app/src/tests/unit/api/geographicAssociation.test.ts delete mode 100644 app/src/tests/unit/hooks/useUserGeographic.test.tsx delete mode 100644 app/src/tests/unit/pages/Populations.page.test.tsx delete mode 100644 app/src/tests/unit/pages/ReportOutput.page.test.tsx delete mode 100644 app/src/tests/unit/pathways/report/ReportPathwayWrapper.test.tsx delete mode 100644 app/src/tests/unit/pathways/report/views/ReportSetupView.test.tsx delete mode 100644 app/src/tests/unit/pathways/simulation/SimulationPathwayWrapper.test.tsx delete mode 100644 app/src/tests/unit/utils/PopulationOps.test.ts delete mode 100644 app/src/tests/unit/utils/populationMatching.test.ts delete mode 100644 app/src/tests/unit/utils/shareUtils.test.ts diff --git a/app/src/adapters/UserGeographicAdapter.ts b/app/src/adapters/UserGeographicAdapter.ts deleted file mode 100644 index 4cc69e700..000000000 --- a/app/src/adapters/UserGeographicAdapter.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { UserGeographyPopulation } from '@/types/ingredients/UserPopulation'; - -/** - * Adapter for converting between UserGeographyPopulation and API formats - */ -export class UserGeographicAdapter { - /** - * Convert UserGeographyPopulation to API creation payload - * Note: This endpoint doesn't exist yet - */ - static toCreationPayload(population: UserGeographyPopulation): any { - return { - user_id: population.userId, - geography_id: population.geographyId, - country_id: population.countryId, - label: population.label, - scope: population.scope, - created_at: population.createdAt, - updated_at: population.updatedAt || new Date().toISOString(), - }; - } - - /** - * Convert API response to UserGeographyPopulation - * Explicitly coerces IDs to strings to handle JSON.parse type mismatches - * Note: API endpoint doesn't exist yet - */ - static fromApiResponse(apiData: any): UserGeographyPopulation { - return { - type: 'geography' as const, - id: String(apiData.geography_id), - userId: String(apiData.user_id), - geographyId: String(apiData.geography_id), - countryId: apiData.country_id, - scope: apiData.scope || 'national', - label: apiData.label, - createdAt: apiData.created_at, - updatedAt: apiData.updated_at, - isCreated: true, - }; - } -} diff --git a/app/src/adapters/index.ts b/app/src/adapters/index.ts index be7779f5c..4e38a3135 100644 --- a/app/src/adapters/index.ts +++ b/app/src/adapters/index.ts @@ -11,7 +11,6 @@ export { RegionsAdapter } from './RegionsAdapter'; export { UserReportAdapter } from './UserReportAdapter'; export { UserSimulationAdapter } from './UserSimulationAdapter'; export { UserHouseholdAdapter } from './UserHouseholdAdapter'; -export { UserGeographicAdapter } from './UserGeographicAdapter'; // Conversion Helpers export { diff --git a/app/src/api/geographicAssociation.ts b/app/src/api/geographicAssociation.ts deleted file mode 100644 index e2b3b5e08..000000000 --- a/app/src/api/geographicAssociation.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { UserGeographicAdapter } from '@/adapters/UserGeographicAdapter'; -import { UserGeographyPopulation } from '@/types/ingredients/UserPopulation'; - -export interface UserGeographicStore { - create: (population: UserGeographyPopulation) => Promise; - findByUser: (userId: string, countryId?: string) => Promise; - findById: (userId: string, geographyId: string) => Promise; - update: ( - userId: string, - geographyId: string, - updates: Partial - ) => Promise; - // The below are not yet implemented, but keeping for future use - // delete(userId: string, geographyId: string): Promise; -} - -export class ApiGeographicStore implements UserGeographicStore { - // TODO: Modify value to match to-be-created API endpoint structure - private readonly BASE_URL = '/api/user-geographic-associations'; - - async create(population: UserGeographyPopulation): Promise { - const payload = UserGeographicAdapter.toCreationPayload(population); - - const response = await fetch(`${this.BASE_URL}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - throw new Error('Failed to create geographic association'); - } - - const apiResponse = await response.json(); - return UserGeographicAdapter.fromApiResponse(apiResponse); - } - - async findByUser(userId: string, countryId?: string): Promise { - const response = await fetch(`${this.BASE_URL}/user/${userId}`, { - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to fetch user associations'); - } - - const apiResponses = await response.json(); - - // Convert each API response to UserGeographyPopulation and filter by country if specified - const geographies = apiResponses.map((apiData: any) => - UserGeographicAdapter.fromApiResponse(apiData) - ); - return countryId - ? geographies.filter((g: UserGeographyPopulation) => g.countryId === countryId) - : geographies; - } - - async findById(userId: string, geographyId: string): Promise { - const response = await fetch(`${this.BASE_URL}/${userId}/${geographyId}`, { - headers: { 'Content-Type': 'application/json' }, - }); - - if (response.status === 404) { - return null; - } - - if (!response.ok) { - throw new Error('Failed to fetch association'); - } - - const apiData = await response.json(); - return UserGeographicAdapter.fromApiResponse(apiData); - } - - async update( - _userId: string, - _geographyId: string, - _updates: Partial - ): Promise { - // TODO: Implement when backend API endpoint is available - // Expected endpoint: PUT /api/user-geographic-associations/:userId/:geographyId - // Expected payload: UserGeographicUpdatePayload (to be created) - - console.warn( - '[ApiGeographicStore.update] API endpoint not yet implemented. ' + - 'This method will be activated when user authentication is added.' - ); - - throw new Error( - 'Geographic population updates via API are not yet supported. ' + - 'Please ensure you are using localStorage mode.' - ); - } - - // Not yet implemented, but keeping for future use - /* - async delete(userId: string, geographyId: string): Promise { - const response = await fetch(`/api/user-geographic-associations/${userId}/${geographyId}`, { - method: 'DELETE', - }); - - if !response.ok) { - throw new Error('Failed to delete association'); - } - } - */ -} - -export class LocalStorageGeographicStore implements UserGeographicStore { - private readonly STORAGE_KEY = 'user-geographic-associations'; - - async create(population: UserGeographyPopulation): Promise { - const newPopulation: UserGeographyPopulation = { - ...population, - createdAt: population.createdAt || new Date().toISOString(), - }; - - const populations = this.getStoredPopulations(); - - // Allow duplicates - users can create multiple entries for the same geography - // Each entry has a unique ID from the caller - const updatedPopulations = [...populations, newPopulation]; - this.setStoredPopulations(updatedPopulations); - - return newPopulation; - } - - async findByUser(userId: string, countryId?: string): Promise { - const populations = this.getStoredPopulations(); - return populations.filter( - (p) => p.userId === userId && (!countryId || p.countryId === countryId) - ); - } - - async findById(userId: string, geographyId: string): Promise { - const populations = this.getStoredPopulations(); - return populations.find((p) => p.userId === userId && p.geographyId === geographyId) || null; - } - - async update( - userId: string, - geographyId: string, - updates: Partial - ): Promise { - const populations = this.getStoredPopulations(); - - // Find by userId and geographyId composite key - const index = populations.findIndex( - (g) => g.userId === userId && g.geographyId === geographyId - ); - - if (index === -1) { - throw new Error( - `UserGeography with userId ${userId} and geographyId ${geographyId} not found` - ); - } - - // Merge updates and set timestamp - const updated: UserGeographyPopulation = { - ...populations[index], - ...updates, - updatedAt: new Date().toISOString(), - }; - - populations[index] = updated; - this.setStoredPopulations(populations); - - return updated; - } - - // Not yet implemented, but keeping for future use - /* - async delete(userId: string, geographyId: string): Promise { - const populations = this.getStoredPopulations(); - const filtered = populations.filter(p => !(p.userId === userId && p.geographyId === geographyId)); - - if (filtered.length === populations.length) { - throw new Error('Geographic population not found'); - } - - this.setStoredPopulations(filtered); - } - */ - - private getStoredPopulations(): UserGeographyPopulation[] { - try { - const stored = localStorage.getItem(this.STORAGE_KEY); - if (!stored) { - return []; - } - - const parsed = JSON.parse(stored); - // Data is already in application format (UserGeographyPopulation), just ensure type coercion - return parsed.map((data: any) => ({ - ...data, - id: String(data.id), - userId: String(data.userId), - geographyId: String(data.geographyId), - })); - } catch { - return []; - } - } - - private setStoredPopulations(populations: UserGeographyPopulation[]): void { - try { - localStorage.setItem(this.STORAGE_KEY, JSON.stringify(populations)); - } catch (error) { - throw new Error('Failed to store geographic populations in local storage'); - } - } - - // Currently unused utility for syncing when user logs in - getAllPopulations(): UserGeographyPopulation[] { - return this.getStoredPopulations(); - } - - clearAllPopulations(): void { - localStorage.removeItem(this.STORAGE_KEY); - } -} diff --git a/app/src/hooks/useSaveSharedReport.ts b/app/src/hooks/useSaveSharedReport.ts index cc13210ed..3159519f9 100644 --- a/app/src/hooks/useSaveSharedReport.ts +++ b/app/src/hooks/useSaveSharedReport.ts @@ -13,7 +13,6 @@ import { ReportIngredientsInput } from '@/hooks/utils/useFetchReportIngredients' import { CountryId } from '@/libs/countries'; import { UserReport } from '@/types/ingredients/UserReport'; import { getShareDataUserReportId } from '@/utils/shareUtils'; -import { useCreateGeographicAssociation } from './useUserGeographic'; import { useCreateHouseholdAssociation } from './useUserHousehold'; import { useCreatePolicyAssociation } from './useUserPolicy'; import { useCreateReportAssociation, useUserReportStore } from './useUserReportAssociations'; @@ -37,7 +36,6 @@ export function useSaveSharedReport() { const createSimulationAssociation = useCreateSimulationAssociation(); const createPolicyAssociation = useCreatePolicyAssociation(); const createHouseholdAssociation = useCreateHouseholdAssociation(); - const createGeographicAssociation = useCreateGeographicAssociation(); const reportStore = useUserReportStore(); // Get currentLawId from static metadata to skip creating associations for current law policies @@ -98,23 +96,14 @@ export function useSaveSharedReport() { }) ); - // Save geographies - const geographyPromises = shareData.userGeographies.map((geo) => - createGeographicAssociation.mutateAsync({ - userId, - geographyId: geo.geographyId, - countryId: geo.countryId as CountryId, - scope: geo.scope, - label: geo.label ?? undefined, - }) - ); + // Note: Geographies are no longer saved as user associations. + // They are constructed from simulation data when needed. // Run all ingredient saves in parallel (best-effort) const allResults = await Promise.allSettled([ ...simPromises, ...policyPromises, ...householdPromises, - ...geographyPromises, ]); // Save the report (required) @@ -158,8 +147,7 @@ export function useSaveSharedReport() { createReportAssociation.isPending || createSimulationAssociation.isPending || createPolicyAssociation.isPending || - createHouseholdAssociation.isPending || - createGeographicAssociation.isPending; + createHouseholdAssociation.isPending; return { saveSharedReport, diff --git a/app/src/hooks/useSharedReportData.ts b/app/src/hooks/useSharedReportData.ts index 8293a86bb..33ebc67f2 100644 --- a/app/src/hooks/useSharedReportData.ts +++ b/app/src/hooks/useSharedReportData.ts @@ -8,10 +8,7 @@ */ import { UserPolicy } from '@/types/ingredients/UserPolicy'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; +import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation'; import { UserReport } from '@/types/ingredients/UserReport'; import { UserSimulation } from '@/types/ingredients/UserSimulation'; import { @@ -33,7 +30,6 @@ interface UseSharedReportDataResult { userSimulations: UserSimulation[]; userPolicies: UserPolicy[]; userHouseholds: UserHouseholdPopulation[]; - userGeographies: UserGeographyPopulation[]; // Base ingredients (fetched from API) report: ReturnType['report']; @@ -74,9 +70,9 @@ export function useSharedReportData( userSimulations: expandedAssociations?.userSimulations ?? [], userPolicies: expandedAssociations?.userPolicies ?? [], userHouseholds: expandedAssociations?.userHouseholds ?? [], - userGeographies: expandedAssociations?.userGeographies ?? [], // Base ingredients from API + // Note: geographies are constructed from simulation data, not user associations report, simulations, policies, diff --git a/app/src/hooks/useUserGeographic.ts b/app/src/hooks/useUserGeographic.ts deleted file mode 100644 index 1a7736db9..000000000 --- a/app/src/hooks/useUserGeographic.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { ApiGeographicStore, LocalStorageGeographicStore } from '@/api/geographicAssociation'; -import { useCurrentCountry } from '@/hooks/useCurrentCountry'; -import { useRegionsList } from '@/hooks/useStaticMetadata'; -import { queryConfig } from '@/libs/queryConfig'; -import { geographicAssociationKeys } from '@/libs/queryKeys'; -import { Geography } from '@/types/ingredients/Geography'; -import { UserGeographyPopulation } from '@/types/ingredients/UserPopulation'; -import { getCountryLabel } from '@/utils/geographyUtils'; -import { extractRegionDisplayValue } from '@/utils/regionStrategies'; - -const apiGeographicStore = new ApiGeographicStore(); -const localGeographicStore = new LocalStorageGeographicStore(); - -export const useUserGeographicStore = () => { - const isLoggedIn = false; // TODO: Replace with actual auth check in future - return isLoggedIn ? apiGeographicStore : localGeographicStore; -}; - -// This fetches only the user-geographic associations -export const useGeographicAssociationsByUser = (userId: string) => { - const store = useUserGeographicStore(); - const countryId = useCurrentCountry(); - const isLoggedIn = false; // TODO: Replace with actual auth check in future - const config = isLoggedIn ? queryConfig.api : queryConfig.localStorage; - - return useQuery({ - queryKey: geographicAssociationKeys.byUser(userId, countryId), - queryFn: () => store.findByUser(userId, countryId), - ...config, - }); -}; - -export const useGeographicAssociation = (userId: string, geographyId: string) => { - const store = useUserGeographicStore(); - const isLoggedIn = false; // TODO: Replace with actual auth check in future - const config = isLoggedIn ? queryConfig.api : queryConfig.localStorage; - - return useQuery({ - queryKey: geographicAssociationKeys.specific(userId, geographyId), - queryFn: () => store.findById(userId, geographyId), - ...config, - }); -}; - -export const useCreateGeographicAssociation = () => { - const store = useUserGeographicStore(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (population: Omit) => - store.create({ ...population, type: 'geography' as const }), - onSuccess: (newPopulation) => { - // Invalidate and refetch related queries - queryClient.invalidateQueries({ - queryKey: geographicAssociationKeys.byUser(newPopulation.userId, newPopulation.countryId), - }); - queryClient.invalidateQueries({ - queryKey: geographicAssociationKeys.byGeography(newPopulation.geographyId), - }); - - // Update specific query cache - queryClient.setQueryData( - geographicAssociationKeys.specific(newPopulation.userId, newPopulation.geographyId), - newPopulation - ); - }, - }); -}; - -export const useUpdateGeographicAssociation = () => { - const store = useUserGeographicStore(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ - userId, - geographyId, - updates, - }: { - userId: string; - geographyId: string; - updates: Partial; - }) => store.update(userId, geographyId, updates), - - onSuccess: (updatedAssociation) => { - // Invalidate all related queries to trigger refetch - queryClient.invalidateQueries({ - queryKey: geographicAssociationKeys.byUser( - updatedAssociation.userId, - updatedAssociation.countryId - ), - }); - - queryClient.invalidateQueries({ - queryKey: geographicAssociationKeys.byGeography(updatedAssociation.geographyId), - }); - - queryClient.invalidateQueries({ - queryKey: geographicAssociationKeys.specific( - updatedAssociation.userId, - updatedAssociation.geographyId - ), - }); - }, - }); -}; - -// Type for the combined data structure -export interface UserGeographicMetadataWithAssociation { - association: UserGeographyPopulation; - geography: Geography | undefined; - isLoading: boolean; - error: Error | null | undefined; - isError?: boolean; -} - -export function isGeographicMetadataWithAssociation( - obj: any -): obj is UserGeographicMetadataWithAssociation { - return ( - obj && - typeof obj === 'object' && - 'association' in obj && - 'geography' in obj && - (obj.geography === undefined || typeof obj.geography === 'object') && - typeof obj.isLoading === 'boolean' && - ('error' in obj ? obj.error === null || obj.error instanceof Error : true) - ); -} - -export const useUserGeographics = (userId: string) => { - // Get regions from static metadata for label lookups - const countryId = useCurrentCountry(); - const regions = useRegionsList(countryId); - - // First, get the populations - const { - data: populations, - isLoading: populationsLoading, - error: populationsError, - } = useGeographicAssociationsByUser(userId); - - // Helper function to get proper label from regions or fallback - const getGeographyName = (population: UserGeographyPopulation): string => { - // If label exists, use it - if (population.label) { - return population.label; - } - - // For national scope, use country name - if (population.scope === 'national') { - return getCountryLabel(population.countryId); - } - - // For subnational, look up in regions - // population.geographyId now contains the FULL prefixed value for UK regions - // e.g., "constituency/Sheffield Central" or "country/england" - if (regions.length > 0) { - // Try exact match first (handles prefixed UK values) - const region = regions.find((r) => r.name === population.geographyId); - - if (region?.label) { - return region.label; - } - - // Fallback: try adding prefixes (for backward compatibility) - const fallbackRegion = regions.find( - (r) => - r.name === `state/${population.geographyId}` || - r.name === `constituency/${population.geographyId}` || - r.name === `country/${population.geographyId}` - ); - - if (fallbackRegion?.label) { - return fallbackRegion.label; - } - } - - // Fallback to geography ID (strip prefix for display if present) - return extractRegionDisplayValue(population.geographyId); - }; - - // For geographic populations, we construct Geography objects from the population data - // since they don't require API fetching like households do - const geographicsWithAssociations: UserGeographicMetadataWithAssociation[] | undefined = - populations?.map((population) => { - // Construct a Geography object from the population data - const geography: Geography = { - id: population.geographyId, - countryId: population.countryId, - scope: population.scope, - geographyId: population.geographyId, - name: getGeographyName(population), - }; - - return { - association: population, - geography, - isLoading: false, - error: null, - isError: false, - }; - }); - - return { - data: geographicsWithAssociations, - isLoading: populationsLoading, - isError: !!populationsError, - error: populationsError, - associations: populations, // Still available if needed separately - }; -}; diff --git a/app/src/hooks/useUserReports.ts b/app/src/hooks/useUserReports.ts index 412246aa7..f721aa2bc 100644 --- a/app/src/hooks/useUserReports.ts +++ b/app/src/hooks/useUserReports.ts @@ -12,15 +12,11 @@ import { Policy } from '@/types/ingredients/Policy'; import { Report } from '@/types/ingredients/Report'; import { Simulation } from '@/types/ingredients/Simulation'; import { UserPolicy } from '@/types/ingredients/UserPolicy'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; +import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation'; import { UserReport } from '@/types/ingredients/UserReport'; import { UserSimulation } from '@/types/ingredients/UserSimulation'; import { householdKeys, policyKeys, reportKeys, simulationKeys } from '../libs/queryKeys'; import { useRegionsList } from './useStaticMetadata'; -import { useGeographicAssociationsByUser } from './useUserGeographic'; import { useHouseholdAssociationsByUser } from './useUserHousehold'; import { usePolicyAssociationsByUser } from './useUserPolicy'; import { useReportAssociationById, useReportAssociationsByUser } from './useUserReportAssociations'; @@ -52,7 +48,6 @@ export interface EnhancedUserReport { userSimulations?: UserSimulation[]; userPolicies?: UserPolicy[]; userHouseholds?: UserHouseholdPopulation[]; - userGeographies?: UserGeographyPopulation[]; // Status isLoading: boolean; @@ -425,7 +420,6 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo const { data: policyAssociations } = usePolicyAssociationsByUser(userId || ''); const { data: householdAssociations } = useHouseholdAssociationsByUser(userId || ''); - const { data: geographyAssociations } = useGeographicAssociationsByUser(userId || ''); const userSimulations = simulationAssociations?.filter((sa) => finalReport?.simulationIds?.includes(sa.simulationId) @@ -492,11 +486,6 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo } }); - // Step 8: Filter geography associations for geographies used in this report - const userGeographies = geographyAssociations?.filter((ga) => - geographies.some((g) => g.id === ga.geographyId) - ); - return { userReport, report: finalReport, @@ -507,7 +496,6 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo userSimulations, userPolicies, userHouseholds, - userGeographies, isLoading: userReportLoading || repLoading || diff --git a/app/src/hooks/utils/useFetchReportIngredients.ts b/app/src/hooks/utils/useFetchReportIngredients.ts index 27f2d0c63..8bb5e6797 100644 --- a/app/src/hooks/utils/useFetchReportIngredients.ts +++ b/app/src/hooks/utils/useFetchReportIngredients.ts @@ -25,10 +25,7 @@ import { Policy } from '@/types/ingredients/Policy'; import { Report } from '@/types/ingredients/Report'; import { Simulation } from '@/types/ingredients/Simulation'; import { UserPolicy } from '@/types/ingredients/UserPolicy'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; +import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation'; import { UserReport } from '@/types/ingredients/UserReport'; import { UserSimulation } from '@/types/ingredients/UserSimulation'; import { combineLoadingStates, extractUniqueIds, useParallelQueries } from './normalizedUtils'; @@ -96,10 +93,6 @@ export type ShareableUserHousehold = Omit< UserHouseholdPopulation, 'userId' | 'createdAt' | 'updatedAt' >; -export type ShareableUserGeography = Omit< - UserGeographyPopulation, - 'userId' | 'createdAt' | 'updatedAt' ->; /** * Input for useFetchReportIngredients @@ -110,7 +103,6 @@ export interface ReportIngredientsInput { userSimulations: ShareableUserSimulation[]; userPolicies: ShareableUserPolicy[]; userHouseholds: ShareableUserHousehold[]; - userGeographies: ShareableUserGeography[]; } /** @@ -139,7 +131,6 @@ export function expandUserAssociations( userSimulations: UserSimulation[]; userPolicies: UserPolicy[]; userHouseholds: UserHouseholdPopulation[]; - userGeographies: UserGeographyPopulation[]; } { return { userReport: { @@ -159,10 +150,6 @@ export function expandUserAssociations( ...h, userId, })), - userGeographies: input.userGeographies.map((g) => ({ - ...g, - userId, - })), }; } diff --git a/app/src/libs/queryKeys.ts b/app/src/libs/queryKeys.ts index 4228ae073..f769dcb3d 100644 --- a/app/src/libs/queryKeys.ts +++ b/app/src/libs/queryKeys.ts @@ -59,18 +59,6 @@ export const householdKeys = { byUser: (userId: string) => [...householdKeys.all, 'user_id', userId] as const, }; -export const geographicAssociationKeys = { - all: ['geographic-associations'] as const, - byUser: (userId: string, countryId?: string) => - countryId - ? ([...geographicAssociationKeys.all, 'user', userId, 'country', countryId] as const) - : ([...geographicAssociationKeys.all, 'user', userId] as const), - byGeography: (geographyId: string) => - [...geographicAssociationKeys.all, 'geography', geographyId] as const, - specific: (userId: string, geographyId: string) => - [...geographicAssociationKeys.all, 'user', userId, 'geography', geographyId] as const, -}; - export const simulationKeys = { all: ['simulations'] as const, byId: (simulationId: string) => [...simulationKeys.all, 'simulation_id', simulationId] as const, diff --git a/app/src/pages/Populations.page.tsx b/app/src/pages/Populations.page.tsx index 5e0e0d7a8..f453fd464 100644 --- a/app/src/pages/Populations.page.tsx +++ b/app/src/pages/Populations.page.tsx @@ -7,40 +7,24 @@ import { RenameIngredientModal } from '@/components/common/RenameIngredientModal import IngredientReadView from '@/components/IngredientReadView'; import { MOCK_USER_ID } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; -import { useRegionsList } from '@/hooks/useStaticMetadata'; -import { - useGeographicAssociationsByUser, - useUpdateGeographicAssociation, -} from '@/hooks/useUserGeographic'; import { useUpdateHouseholdAssociation, useUserHouseholds } from '@/hooks/useUserHousehold'; -import { countryIds } from '@/libs/countries'; -import { UserGeographyPopulation } from '@/types/ingredients/UserPopulation'; import { formatDate } from '@/utils/dateUtils'; -import { getCountryLabel } from '@/utils/geographyUtils'; -import { extractRegionDisplayValue } from '@/utils/regionStrategies'; export default function PopulationsPage() { const userId = MOCK_USER_ID.toString(); // TODO: Replace with actual user ID retrieval logic // TODO: Session storage hard-fixes "anonymous" as user ID; this should really just be anything const countryId = useCurrentCountry(); - const regions = useRegionsList(countryId); // Fetch household associations + // Note: Geographic populations are no longer stored as user associations. + // They are selected per-simulation and constructed from metadata. const { data: householdData, - isLoading: isHouseholdLoading, - isError: isHouseholdError, - error: householdError, + isLoading, + isError, + error, } = useUserHouseholds(userId); - // Fetch geographic associations - const { - data: geographicData, - isLoading: isGeographicLoading, - isError: isGeographicError, - error: geographicError, - } = useGeographicAssociationsByUser(userId); - const navigate = useNavigate(); const [searchValue, setSearchValue] = useState(''); @@ -48,18 +32,10 @@ export default function PopulationsPage() { // Rename modal state const [renamingId, setRenamingId] = useState(null); - const [renamingType, setRenamingType] = useState<'household' | 'geography' | null>(null); - const [renamingUserId, setRenamingUserId] = useState(null); const [renameOpened, { open: openRename, close: closeRename }] = useDisclosure(false); // Rename mutation hooks const updateHouseholdAssociation = useUpdateHouseholdAssociation(); - const updateGeographicAssociation = useUpdateGeographicAssociation(); - - // Combined loading and error states - const isLoading = isHouseholdLoading || isGeographicLoading; - const isError = isHouseholdError || isGeographicError; - const error = householdError || geographicError; const handleBuildPopulation = () => { navigate(`/${countryId}/households/create`); @@ -74,134 +50,46 @@ export default function PopulationsPage() { const isSelected = (recordId: string) => selectedIds.includes(recordId); const handleOpenRename = (recordId: string) => { - // Determine type by looking up in the original data - // Households use their association.id, geographies use geographyId + // Find the household by its association id const household = householdData?.find( (item) => (item.association.id || item.association.householdId.toString()) === recordId ); - const geography = geographicData?.find((item) => item.geographyId === recordId); - if (!household && !geography) { + if (!household) { return; } - const type: 'household' | 'geography' = household ? 'household' : 'geography'; - const userIdValue = household ? household.association.userId : geography!.userId; - setRenamingId(recordId); - setRenamingType(type); - setRenamingUserId(userIdValue); openRename(); }; const handleCloseRename = () => { closeRename(); setRenamingId(null); - setRenamingType(null); - setRenamingUserId(null); }; const handleRename = async (newLabel: string) => { - if (!renamingId || !renamingType || !renamingUserId) { + if (!renamingId) { return; } try { - if (renamingType === 'household') { - await updateHouseholdAssociation.mutateAsync({ - userHouseholdId: renamingId, - updates: { label: newLabel }, - }); - } else { - // For geographies, renamingId is the geographyId - await updateGeographicAssociation.mutateAsync({ - userId: renamingUserId, - geographyId: renamingId, - updates: { label: newLabel }, - }); - } + await updateHouseholdAssociation.mutateAsync({ + userHouseholdId: renamingId, + updates: { label: newLabel }, + }); handleCloseRename(); - } catch (error) { - console.error(`[PopulationsPage] Failed to rename ${renamingType}:`, error); + } catch (err) { + console.error(`[PopulationsPage] Failed to rename household:`, err); } }; // Find the item being renamed for current label const renamingHousehold = householdData?.find((item) => item.association.id === renamingId); - const renamingGeography = geographicData?.find((item) => item.id === renamingId); const currentLabel = - renamingType === 'household' - ? renamingHousehold?.association.label || - `Household #${renamingHousehold?.association.householdId}` - : renamingGeography?.label || ''; - - // Helper function to get geographic scope details - const getGeographicDetails = (geography: UserGeographyPopulation) => { - const details = []; - - // Add geography scope - const typeLabel = geography.scope === 'national' ? 'National' : 'Subnational'; - details.push({ text: typeLabel, badge: '' }); - - // Add region if subnational - if (geography.scope === 'subnational' && geography.geographyId) { - // geography.geographyId now contains FULL prefixed value for UK regions - // e.g., "constituency/Sheffield Central" or "country/england" - let regionLabel = geography.geographyId; - const fullRegionName = geography.geographyId; // Track the full name with prefix - if (regions.length > 0) { - // Try exact match first (handles prefixed UK values and US state codes) - const region = regions.find((r) => r.name === geography.geographyId); - - if (region) { - regionLabel = region.label; - } else { - // Fallback: try adding prefixes for backward compatibility - const fallbackRegion = regions.find( - (r) => - r.name === `state/${geography.geographyId}` || - r.name === `constituency/${geography.geographyId}` || - r.name === `country/${geography.geographyId}` - ); - if (fallbackRegion) { - regionLabel = fallbackRegion.label; - } - } - } - - // If still no label found, strip prefix for display - if (regionLabel === geography.geographyId) { - regionLabel = extractRegionDisplayValue(geography.geographyId); - } - - // Determine region type based on country and prefix - let regionTypeLabel = 'Region'; - if (geography.countryId === 'us') { - regionTypeLabel = 'State'; - } else if (geography.countryId === 'uk') { - if (fullRegionName.startsWith('country/')) { - regionTypeLabel = 'Country'; - } else if (fullRegionName.startsWith('constituency/')) { - regionTypeLabel = 'Constituency'; - } - } - - // For UK constituencies, show both country and constituency - if (geography.countryId === 'uk' && fullRegionName.startsWith('constituency/')) { - const countryLabel = getCountryLabel(geography.countryId); - details.push({ text: countryLabel, badge: '' }); - } - - details.push({ text: `${regionTypeLabel}: ${regionLabel}`, badge: '' }); - } else { - // National scope - just show country - const countryLabel = getCountryLabel(geography.countryId); - details.push({ text: countryLabel, badge: '' }); - } - - return details; - }; + renamingHousehold?.association.label || + `Household #${renamingHousehold?.association.householdId}`; // Helper function to get household configuration details const getHouseholdDetails = (household: any) => { @@ -250,7 +138,9 @@ export default function PopulationsPage() { ]; // Transform household data - const householdRecords: IngredientRecord[] = + // Note: Geographic populations are no longer stored as user associations. + // They are selected per-simulation and don't appear in this list. + const transformedData: IngredientRecord[] = householdData?.map((item) => { const detailsItems = getHouseholdDetails(item.household); @@ -277,38 +167,6 @@ export default function PopulationsPage() { }; }) || []; - // Transform geographic data - const geographicRecords: IngredientRecord[] = - geographicData?.map((association) => { - const detailsItems = getGeographicDetails(association); - - return { - id: association.geographyId, - type: 'geography', - userId: association.userId, - geographyId: association.geographyId, - populationName: { - text: association.label, - } as TextValue, - dateCreated: { - text: association.createdAt - ? formatDate( - association.createdAt, - 'short-month-day-year', - association?.countryId as (typeof countryIds)[number], - true - ) - : '', - } as TextValue, - details: { - items: detailsItems, - } as BulletsValue, - }; - }) || []; - - // Combine both data sources - const transformedData: IngredientRecord[] = [...householdRecords, ...geographicRecords]; - return ( <> @@ -336,12 +194,8 @@ export default function PopulationsPage() { onClose={handleCloseRename} currentLabel={currentLabel} onRename={handleRename} - isLoading={ - renamingType === 'household' - ? updateHouseholdAssociation.isPending - : updateGeographicAssociation.isPending - } - ingredientType={renamingType || 'household'} + isLoading={updateHouseholdAssociation.isPending} + ingredientType="household" /> ); diff --git a/app/src/pages/ReportOutput.page.tsx b/app/src/pages/ReportOutput.page.tsx index 6efd00c41..c7a1a0e20 100644 --- a/app/src/pages/ReportOutput.page.tsx +++ b/app/src/pages/ReportOutput.page.tsx @@ -96,7 +96,6 @@ export default function ReportOutputPage() { userSimulations, userPolicies, userHouseholds, - userGeographies, isLoading: dataLoading, error: dataError, } = data; @@ -187,12 +186,13 @@ export default function ReportOutputPage() { } // Create ShareData from user associations + // Note: Geographies are no longer stored as user associations - they're + // constructed from simulation data when needed const shareDataToEncode = createShareData( userReport, userSimulations ?? [], userPolicies ?? [], - userHouseholds ?? [], - userGeographies ?? [] + userHouseholds ?? [] ); if (!shareDataToEncode) { @@ -302,7 +302,6 @@ export default function ReportOutputPage() { userPolicies={userPolicies} policies={policies} geographies={geographies} - userGeographies={userGeographies} /> ); } diff --git a/app/src/pages/report-output/GeographySubPage.tsx b/app/src/pages/report-output/GeographySubPage.tsx index d59184886..bba226d74 100644 --- a/app/src/pages/report-output/GeographySubPage.tsx +++ b/app/src/pages/report-output/GeographySubPage.tsx @@ -1,14 +1,11 @@ import { Box, Table, Text } from '@mantine/core'; import { colors, spacing, typography } from '@/designTokens'; import { Geography } from '@/types/ingredients/Geography'; -import { UserGeographyPopulation } from '@/types/ingredients/UserPopulation'; import { capitalize } from '@/utils/stringUtils'; interface GeographySubPageProps { baselineGeography?: Geography; reformGeography?: Geography; - baselineUserGeography?: UserGeographyPopulation; - reformUserGeography?: UserGeographyPopulation; } /** @@ -16,12 +13,14 @@ interface GeographySubPageProps { * * Shows baseline and reform geographies side-by-side in a comparison table. * Collapses columns when both simulations use the same geography. + * + * Note: Geography names come directly from the Geography objects (constructed from + * simulation data), not from user associations since geographies are no longer + * stored as user associations. */ export default function GeographySubPage({ baselineGeography, reformGeography, - baselineUserGeography, - reformUserGeography, }: GeographySubPageProps) { if (!baselineGeography && !reformGeography) { return
No geography data available
; @@ -30,9 +29,9 @@ export default function GeographySubPage({ // Check if geographies are the same const geographiesAreSame = baselineGeography?.id === reformGeography?.id; - // Get labels from UserGeographyPopulation, fallback to geography names, then to generic labels - const baselineLabel = baselineUserGeography?.label || baselineGeography?.name || 'Baseline'; - const reformLabel = reformUserGeography?.label || reformGeography?.name || 'Reform'; + // Get labels from geography names + const baselineLabel = baselineGeography?.name || 'Baseline'; + const reformLabel = reformGeography?.name || 'Reform'; // Define table rows const rows = [ diff --git a/app/src/pages/report-output/PopulationSubPage.tsx b/app/src/pages/report-output/PopulationSubPage.tsx index fecb672da..f26f3371c 100644 --- a/app/src/pages/report-output/PopulationSubPage.tsx +++ b/app/src/pages/report-output/PopulationSubPage.tsx @@ -1,10 +1,7 @@ import { Geography } from '@/types/ingredients/Geography'; import { Household } from '@/types/ingredients/Household'; import { Simulation } from '@/types/ingredients/Simulation'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; +import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation'; import GeographySubPage from './GeographySubPage'; import HouseholdSubPage from './HouseholdSubPage'; @@ -14,7 +11,6 @@ interface PopulationSubPageProps { households?: Household[]; geographies?: Geography[]; userHouseholds?: UserHouseholdPopulation[]; - userGeographies?: UserGeographyPopulation[]; } /** @@ -29,7 +25,6 @@ export default function PopulationSubPage({ households, geographies, userHouseholds, - userGeographies, }: PopulationSubPageProps) { // Determine population type from simulations const populationType = baselineSimulation?.populationType || reformSimulation?.populationType; @@ -58,6 +53,7 @@ export default function PopulationSubPage({ } // Handle geography population type + // Note: Geographies are constructed from simulation data, not user associations if (populationType === 'geography') { // Extract geography IDs from simulations const baselineGeographyId = baselineSimulation?.populationId; @@ -67,18 +63,10 @@ export default function PopulationSubPage({ const baselineGeography = geographies?.find((g) => g.id === baselineGeographyId); const reformGeography = geographies?.find((g) => g.id === reformGeographyId); - // Find the user geography associations - const baselineUserGeography = userGeographies?.find( - (ug) => ug.geographyId === baselineGeographyId - ); - const reformUserGeography = userGeographies?.find((ug) => ug.geographyId === reformGeographyId); - return ( ); } diff --git a/app/src/pages/report-output/SocietyWideReportOutput.tsx b/app/src/pages/report-output/SocietyWideReportOutput.tsx index 967be7cb3..e34d3fe21 100644 --- a/app/src/pages/report-output/SocietyWideReportOutput.tsx +++ b/app/src/pages/report-output/SocietyWideReportOutput.tsx @@ -9,7 +9,6 @@ import type { Policy } from '@/types/ingredients/Policy'; import type { Report } from '@/types/ingredients/Report'; import type { Simulation } from '@/types/ingredients/Simulation'; import type { UserPolicy } from '@/types/ingredients/UserPolicy'; -import type { UserGeographyPopulation } from '@/types/ingredients/UserPopulation'; import type { UserSimulation } from '@/types/ingredients/UserSimulation'; import { getDisplayStatus } from '@/utils/statusMapping'; import { ComparativeAnalysisPage } from './ComparativeAnalysisPage'; @@ -33,7 +32,6 @@ interface SocietyWideReportOutputProps { userPolicies?: UserPolicy[]; policies?: Policy[]; geographies?: Geography[]; - userGeographies?: UserGeographyPopulation[]; } /** @@ -52,7 +50,6 @@ export function SocietyWideReportOutput({ userPolicies, policies, geographies, - userGeographies, }: SocietyWideReportOutputProps) { // Get calculation status for report (for state decisions) const calcStatus = useCalculationStatus(report?.id || '', 'report'); @@ -159,7 +156,6 @@ export function SocietyWideReportOutput({ baselineSimulation={simulations?.[0]} reformSimulation={simulations?.[1]} geographies={geographies} - userGeographies={userGeographies} /> ); diff --git a/app/src/pathways/report/ReportPathwayWrapper.tsx b/app/src/pathways/report/ReportPathwayWrapper.tsx index 017e1f05c..e030b8a82 100644 --- a/app/src/pathways/report/ReportPathwayWrapper.tsx +++ b/app/src/pathways/report/ReportPathwayWrapper.tsx @@ -17,7 +17,6 @@ import { ReportYearProvider } from '@/contexts/ReportYearContext'; import { useCreateReport } from '@/hooks/useCreateReport'; import { usePathwayNavigation } from '@/hooks/usePathwayNavigation'; import { useCurrentLawId, useRegionsList } from '@/hooks/useStaticMetadata'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; import { useUserHouseholds } from '@/hooks/useUserHousehold'; import { useUserSimulations } from '@/hooks/useUserSimulations'; import { countryIds } from '@/libs/countries'; @@ -103,10 +102,11 @@ export default function ReportPathwayWrapper({ onComplete }: ReportPathwayWrappe const userId = MOCK_USER_ID.toString(); const { data: userSimulations } = useUserSimulations(userId); const { data: userHouseholds } = useUserHouseholds(userId); - const { data: userGeographics } = useUserGeographics(userId); const hasExistingSimulations = (userSimulations?.length ?? 0) > 0; - const hasExistingPopulations = (userHouseholds?.length ?? 0) + (userGeographics?.length ?? 0) > 0; + // Note: Geographic populations are no longer stored as user associations. + // They are selected per-simulation. We only check for existing households. + const hasExistingPopulations = (userHouseholds?.length ?? 0) > 0; // ========== HELPER: Get active simulation ========== const activeSimulation = reportState.simulations[activeSimulationIndex]; diff --git a/app/src/pathways/report/views/ReportSetupView.tsx b/app/src/pathways/report/views/ReportSetupView.tsx index ae5bd057b..7427157c9 100644 --- a/app/src/pathways/report/views/ReportSetupView.tsx +++ b/app/src/pathways/report/views/ReportSetupView.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; import PathwayView from '@/components/common/PathwayView'; import { MOCK_USER_ID } from '@/constants'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; import { useUserHouseholds } from '@/hooks/useUserHousehold'; import { ReportStateProps, SimulationStateProps } from '@/types/pathwayState'; import { isSimulationConfigured } from '@/utils/validation/ingredientValidation'; @@ -32,16 +31,17 @@ export default function ReportSetupView({ const simulation2 = reportState.simulations[1]; // Fetch population data for pre-filling simulation 2 + // Note: Geographic populations are no longer stored as user associations. + // They are selected per-simulation. const userId = MOCK_USER_ID.toString(); const { data: householdData } = useUserHouseholds(userId); - const { data: geographicData } = useUserGeographics(userId); // Check if simulations are fully configured const simulation1Configured = isSimulationConfigured(simulation1); const simulation2Configured = isSimulationConfigured(simulation2); - // Check if population data is loaded (needed for simulation2 prefill) - const isPopulationDataLoaded = householdData !== undefined && geographicData !== undefined; + // Check if household population data is loaded (needed for simulation2 prefill) + const isPopulationDataLoaded = householdData !== undefined; // Determine if simulation2 is optional based on population type of simulation1 const isHouseholdReport = simulation1?.population.type === 'household'; diff --git a/app/src/pathways/report/views/ReportSimulationSelectionView.tsx b/app/src/pathways/report/views/ReportSimulationSelectionView.tsx index 925707f93..1466783e3 100644 --- a/app/src/pathways/report/views/ReportSimulationSelectionView.tsx +++ b/app/src/pathways/report/views/ReportSimulationSelectionView.tsx @@ -5,7 +5,6 @@ import PathwayView from '@/components/common/PathwayView'; import { ButtonPanelVariant } from '@/components/flowView'; import { MOCK_USER_ID } from '@/constants'; import { useCreateSimulation } from '@/hooks/useCreateSimulation'; -import { useCreateGeographicAssociation } from '@/hooks/useUserGeographic'; import { useUserSimulations } from '@/hooks/useUserSimulations'; import { Simulation } from '@/types/ingredients/Simulation'; import { PolicyStateProps, PopulationStateProps, SimulationStateProps } from '@/types/pathwayState'; @@ -106,7 +105,6 @@ export default function ReportSimulationSelectionView({ const [selectedAction, setSelectedAction] = useState(null); const [isCreatingBaseline, setIsCreatingBaseline] = useState(false); - const { mutateAsync: createGeographicAssociation } = useCreateGeographicAssociation(); const simulationLabel = getDefaultBaselineLabel(countryId); const { createSimulation } = useCreateSimulation(simulationLabel); @@ -158,6 +156,8 @@ export default function ReportSimulationSelectionView({ /** * Creates a new default baseline simulation + * Note: Geographies are no longer stored as user associations. The geography + * is constructed from simulation data using the countryId as the geographyId. */ async function createNewBaseline() { if (!onSelectDefaultBaseline) { @@ -166,21 +166,12 @@ export default function ReportSimulationSelectionView({ setIsCreatingBaseline(true); const countryName = countryNames[countryId] || countryId.toUpperCase(); + const geographyId = countryId; // National geography uses countryId try { - // Create geography association - const geographyResult = await createGeographicAssociation({ - id: `${userId}-${Date.now()}`, - userId, - countryId: countryId as any, - geographyId: countryId, - scope: 'national', - label: `${countryName} nationwide`, - }); - - // Create simulation + // Create simulation directly - geography is not stored as user association const simulationData: Partial = { - populationId: geographyResult.geographyId, + populationId: geographyId, policyId: currentLawId.toString(), populationType: 'geography', }; @@ -193,11 +184,7 @@ export default function ReportSimulationSelectionView({ const simulationId = data.result.simulation_id; const policy = createCurrentLawPolicy(currentLawId); - const population = createNationwidePopulation( - countryId, - geographyResult.geographyId, - countryName - ); + const population = createNationwidePopulation(countryId, geographyId, countryName); const simulationState = createSimulationState( simulationId, simulationLabel, @@ -216,10 +203,7 @@ export default function ReportSimulationSelectionView({ }, }); } catch (error) { - console.error( - '[ReportSimulationSelectionView] Failed to create geographic association:', - error - ); + console.error('[ReportSimulationSelectionView] Failed to create simulation:', error); setIsCreatingBaseline(false); } } diff --git a/app/src/pathways/report/views/population/GeographicConfirmationView.tsx b/app/src/pathways/report/views/population/GeographicConfirmationView.tsx index 4527ea17a..b60d66057 100644 --- a/app/src/pathways/report/views/population/GeographicConfirmationView.tsx +++ b/app/src/pathways/report/views/population/GeographicConfirmationView.tsx @@ -1,14 +1,10 @@ /** - * GeographicConfirmationView - View for confirming geographic population - * Duplicated from GeographicConfirmationFrame - * Props-based instead of Redux-based + * GeographicConfirmationView - View for confirming geographic population selection + * Users can select a geography for simulation without creating a user association */ import { Stack, Text } from '@mantine/core'; import PathwayView from '@/components/common/PathwayView'; -import { MOCK_USER_ID } from '@/constants'; -import { useCreateGeographicAssociation } from '@/hooks/useUserGeographic'; -import { UserGeographyPopulation } from '@/types/ingredients/UserPopulation'; import { MetadataRegionEntry } from '@/types/metadata'; import { PopulationStateProps } from '@/types/pathwayState'; import { getCountryLabel, getRegionLabel, getRegionTypeLabel } from '@/utils/geographyUtils'; @@ -26,36 +22,14 @@ export default function GeographicConfirmationView({ onSubmitSuccess, onBack, }: GeographicConfirmationViewProps) { - const { mutateAsync: createGeographicAssociation, isPending } = useCreateGeographicAssociation(); - const currentUserId = MOCK_USER_ID; - - // Build geographic population data from existing geography - const buildGeographicPopulation = (): Omit => { + const handleSubmit = () => { if (!population?.geography) { - throw new Error('No geography found in population state'); + return; } - const basePopulation = { - id: `${currentUserId}-${Date.now()}`, - userId: currentUserId, - countryId: population.geography.countryId, - geographyId: population.geography.geographyId, - scope: population.geography.scope, - label: population.label || population.geography.name || undefined, - }; - - return basePopulation; - }; - - const handleSubmit = async () => { - const populationData = buildGeographicPopulation(); - - try { - const result = await createGeographicAssociation(populationData); - onSubmitSuccess(result.geographyId, result.label || ''); - } catch (err) { - // Error is handled by the mutation - } + const geographyId = population.geography.geographyId; + const label = population.geography.name || getRegionLabel(geographyId, regions); + onSubmitSuccess(geographyId, label); }; // Build display content based on geographic scope @@ -109,7 +83,6 @@ export default function GeographicConfirmationView({ const primaryAction = { label: 'Create household collection', onClick: handleSubmit, - isLoading: isPending, }; return ( diff --git a/app/src/pathways/report/views/population/PopulationExistingView.tsx b/app/src/pathways/report/views/population/PopulationExistingView.tsx index 653b98448..ea03a1719 100644 --- a/app/src/pathways/report/views/population/PopulationExistingView.tsx +++ b/app/src/pathways/report/views/population/PopulationExistingView.tsx @@ -1,7 +1,9 @@ /** - * PopulationExistingView - View for selecting existing population - * Duplicated from SimulationSelectExistingPopulationFrame - * Props-based instead of Redux-based + * PopulationExistingView - View for selecting existing household population + * + * Note: Geographic populations are no longer stored as user associations. + * Users select a geography per-simulation via the scope selection flow. + * This view now only shows household populations. */ import { useState } from 'react'; @@ -9,13 +11,6 @@ import { Text } from '@mantine/core'; import { HouseholdAdapter } from '@/adapters'; import PathwayView from '@/components/common/PathwayView'; import { MOCK_USER_ID } from '@/constants'; -import { useCurrentCountry } from '@/hooks/useCurrentCountry'; -import { useRegionsList } from '@/hooks/useStaticMetadata'; -import { - isGeographicMetadataWithAssociation, - UserGeographicMetadataWithAssociation, - useUserGeographics, -} from '@/hooks/useUserGeographic'; import { isHouseholdMetadataWithAssociation, UserHouseholdMetadataWithAssociation, @@ -23,14 +18,12 @@ import { } from '@/hooks/useUserHousehold'; import { Geography } from '@/types/ingredients/Geography'; import { Household } from '@/types/ingredients/Household'; -import { getCountryLabel, getRegionLabel } from '@/utils/geographyUtils'; -import { - isGeographicAssociationReady, - isHouseholdAssociationReady, -} from '@/utils/validation/ingredientValidation'; +import { isHouseholdAssociationReady } from '@/utils/validation/ingredientValidation'; interface PopulationExistingViewProps { onSelectHousehold: (householdId: string, household: Household, label: string) => void; + // Keep onSelectGeography for API compatibility, but it won't be called from this view + // since users now select geography via the scope flow, not from saved associations onSelectGeography: (geographyId: string, geography: Geography, label: string) => void; onBack?: () => void; onCancel?: () => void; @@ -38,53 +31,29 @@ interface PopulationExistingViewProps { export default function PopulationExistingView({ onSelectHousehold, - onSelectGeography, onBack, onCancel, }: PopulationExistingViewProps) { const userId = MOCK_USER_ID.toString(); - const countryId = useCurrentCountry(); - const regions = useRegionsList(countryId); - // Fetch household populations + // Fetch household populations only + // Geographic populations are no longer stored as user associations const { data: householdData, - isLoading: isHouseholdLoading, - isError: isHouseholdError, - error: householdError, + isLoading, + isError, + error, } = useUserHouseholds(userId); - // Fetch geographic populations - const { - data: geographicData, - isLoading: isGeographicLoading, - isError: isGeographicError, - error: geographicError, - } = useUserGeographics(userId); - - const [localPopulation, setLocalPopulation] = useState< - UserHouseholdMetadataWithAssociation | UserGeographicMetadataWithAssociation | null - >(null); - - // Combined loading and error states - const isLoading = isHouseholdLoading || isGeographicLoading; - const isError = isHouseholdError || isGeographicError; - const error = householdError || geographicError; + const [localPopulation, setLocalPopulation] = + useState(null); function canProceed() { if (!localPopulation) { return false; } - if (isHouseholdMetadataWithAssociation(localPopulation)) { - return isHouseholdAssociationReady(localPopulation); - } - - if (isGeographicMetadataWithAssociation(localPopulation)) { - return isGeographicAssociationReady(localPopulation); - } - - return false; + return isHouseholdAssociationReady(localPopulation); } function handleHouseholdPopulationSelect(association: UserHouseholdMetadataWithAssociation) { @@ -95,28 +64,16 @@ export default function PopulationExistingView({ setLocalPopulation(association); } - function handleGeographicPopulationSelect(association: UserGeographicMetadataWithAssociation) { - if (!association) { - return; - } - - setLocalPopulation(association); - } - function handleSubmit() { if (!localPopulation) { return; } - if (isHouseholdMetadataWithAssociation(localPopulation)) { - handleSubmitHouseholdPopulation(); - } else if (isGeographicMetadataWithAssociation(localPopulation)) { - handleSubmitGeographicPopulation(); - } + handleSubmitHouseholdPopulation(); } function handleSubmitHouseholdPopulation() { - if (!localPopulation || !isHouseholdMetadataWithAssociation(localPopulation)) { + if (!localPopulation) { return; } @@ -134,7 +91,7 @@ export default function PopulationExistingView({ householdToSet = HouseholdAdapter.fromMetadata(localPopulation.household); } else { // Already transformed format from cache - householdToSet = localPopulation.household as any; + householdToSet = localPopulation.household as unknown as Household; } const label = localPopulation.association?.label || ''; @@ -144,21 +101,7 @@ export default function PopulationExistingView({ onSelectHousehold(householdId, householdToSet, label); } - function handleSubmitGeographicPopulation() { - if (!localPopulation || !isGeographicMetadataWithAssociation(localPopulation)) { - return; - } - - const label = localPopulation.association?.label || ''; - const geography = localPopulation.geography!; - const geographyId = geography.id!; - - // Call parent callback instead of dispatching to Redux - onSelectGeography(geographyId, geography, label); - } - const householdPopulations = householdData || []; - const geographicPopulations = geographicData || []; if (isLoading) { return ( @@ -180,7 +123,7 @@ export default function PopulationExistingView({ ); } - if (householdPopulations.length === 0 && geographicPopulations.length === 0) { + if (householdPopulations.length === 0) { return ( isHouseholdMetadataWithAssociation(association) ); - // Combine all populations (pagination handled by PathwayView) - const allPopulations = [...filteredHouseholds, ...geographicPopulations]; - - // Build card list items from ALL household populations - const householdCardItems = allPopulations - .filter((association) => isHouseholdMetadataWithAssociation(association)) - .map((association) => { - const isReady = isHouseholdAssociationReady(association); - - let title = ''; - let subtitle = ''; + // Build card list items from household populations + const cardListItems = filteredHouseholds.map((association) => { + const isReady = isHouseholdAssociationReady(association); - if (!isReady) { - // NOT LOADED YET - show loading indicator - title = '⏳ Loading...'; - subtitle = 'Household data not loaded yet'; - } else if ('label' in association.association && association.association.label) { - title = association.association.label; - subtitle = `Population #${association.household!.id}`; - } else { - title = `Population #${association.household!.id}`; - subtitle = ''; - } + let title = ''; + let subtitle = ''; - return { - id: association.association.id?.toString() || association.household?.id?.toString(), // Use association ID for unique key - title, - subtitle, - onClick: () => handleHouseholdPopulationSelect(association!), - isSelected: - isHouseholdMetadataWithAssociation(localPopulation) && - localPopulation.household?.id === association.household?.id, - }; - }); - - // Helper function to get geographic label from metadata - const getGeographicLabel = (geography: Geography) => { - if (!geography) { - return 'Unknown Location'; - } - - // If it's a national scope, return the country name - if (geography.scope === 'national') { - return getCountryLabel(geography.countryId); - } - - // For subnational, look up in regions - if (geography.scope === 'subnational') { - return getRegionLabel(geography.geographyId, regions); + if (!isReady) { + // NOT LOADED YET - show loading indicator + title = '⏳ Loading...'; + subtitle = 'Household data not loaded yet'; + } else if ('label' in association.association && association.association.label) { + title = association.association.label; + subtitle = `Population #${association.household!.id}`; + } else { + title = `Population #${association.household!.id}`; + subtitle = ''; } - return geography.name || geography.geographyId; - }; - - // Build card list items from ALL geographic populations - const geographicCardItems = allPopulations - .filter((association) => isGeographicMetadataWithAssociation(association)) - .map((association) => { - let title = ''; - let subtitle = ''; - - // Use the label if it exists, otherwise look it up from metadata - if ('label' in association.association && association.association.label) { - title = association.association.label; - } else { - title = getGeographicLabel(association.geography!); - } - - // If user has defined a label, show the geography name as a subtitle (e.g., 'New York'); - // if user has not defined label, we already show geography name above; show nothing - if ('label' in association.association && association.association.label) { - subtitle = getGeographicLabel(association.geography!); - } else { - subtitle = ''; - } - - return { - id: association.association.id?.toString() || association.geography?.id?.toString(), // Use association ID for unique key - title, - subtitle, - onClick: () => handleGeographicPopulationSelect(association!), - isSelected: - isGeographicMetadataWithAssociation(localPopulation) && - localPopulation.geography?.id === association.geography!.id, - }; - }); - // Combine both types of populations - const cardListItems = [...householdCardItems, ...geographicCardItems]; + return { + id: association.association.id?.toString() || association.household?.id?.toString(), + title, + subtitle, + onClick: () => handleHouseholdPopulationSelect(association), + isSelected: localPopulation?.household?.id === association.household?.id, + }; + }); const primaryAction = { label: 'Next', diff --git a/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx b/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx index 3d2a008a5..c5990f0cf 100644 --- a/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx +++ b/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx @@ -7,7 +7,6 @@ import { useState } from 'react'; import PathwayView from '@/components/common/PathwayView'; import { MOCK_USER_ID } from '@/constants'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; import { useUserHouseholds } from '@/hooks/useUserHousehold'; import { PopulationStateProps, SimulationStateProps } from '@/types/pathwayState'; import { getPopulationLabel, getSimulationLabel } from '@/utils/populationCompatibility'; @@ -42,8 +41,9 @@ export default function SimulationPopulationSetupView({ }: SimulationPopulationSetupViewProps) { const userId = MOCK_USER_ID.toString(); const { data: userHouseholds } = useUserHouseholds(userId); - const { data: userGeographics } = useUserGeographics(userId); - const hasExistingPopulations = (userHouseholds?.length ?? 0) + (userGeographics?.length ?? 0) > 0; + // Note: Geographic populations are no longer stored as user associations. + // They are selected per-simulation. We only check for existing households. + const hasExistingPopulations = (userHouseholds?.length ?? 0) > 0; const [selectedAction, setSelectedAction] = useState(null); diff --git a/app/src/pathways/simulation/SimulationPathwayWrapper.tsx b/app/src/pathways/simulation/SimulationPathwayWrapper.tsx index 8927b4542..5d5416f49 100644 --- a/app/src/pathways/simulation/SimulationPathwayWrapper.tsx +++ b/app/src/pathways/simulation/SimulationPathwayWrapper.tsx @@ -13,7 +13,6 @@ import { MOCK_USER_ID } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { usePathwayNavigation } from '@/hooks/usePathwayNavigation'; import { useCurrentLawId, useRegionsList } from '@/hooks/useStaticMetadata'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; import { useUserHouseholds } from '@/hooks/useUserHousehold'; import { useUserPolicies } from '@/hooks/useUserPolicy'; import { RootState } from '@/store'; @@ -62,7 +61,6 @@ export default function SimulationPathwayWrapper({ onComplete }: SimulationPathw }); // Get metadata for population views - const metadata = useSelector((state: RootState) => state.metadata); const currentLawId = useCurrentLawId(countryId); const regionData = useRegionsList(countryId); @@ -75,10 +73,11 @@ export default function SimulationPathwayWrapper({ onComplete }: SimulationPathw const userId = MOCK_USER_ID.toString(); const { data: userPolicies } = useUserPolicies(userId); const { data: userHouseholds } = useUserHouseholds(userId); - const { data: userGeographics } = useUserGeographics(userId); const hasExistingPolicies = (userPolicies?.length ?? 0) > 0; - const hasExistingPopulations = (userHouseholds?.length ?? 0) + (userGeographics?.length ?? 0) > 0; + // Note: Geographic populations are no longer stored as user associations. + // They are selected per-simulation. We only check for existing households. + const hasExistingPopulations = (userHouseholds?.length ?? 0) > 0; // ========== CONDITIONAL NAVIGATION HANDLERS ========== // Skip selection view if user has no existing items diff --git a/app/src/tests/fixtures/api/associationFixtures.ts b/app/src/tests/fixtures/api/associationFixtures.ts index 09af20a63..83857a61f 100644 --- a/app/src/tests/fixtures/api/associationFixtures.ts +++ b/app/src/tests/fixtures/api/associationFixtures.ts @@ -1,8 +1,5 @@ import { UserPolicy } from '@/types/ingredients/UserPolicy'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; +import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation'; import { UserReport } from '@/types/ingredients/UserReport'; import { UserSimulation } from '@/types/ingredients/UserSimulation'; @@ -59,19 +56,7 @@ export const createMockHouseholdAssociation = ( ...overrides, }); -// Mock UserGeographyPopulation -export const createMockGeographyAssociation = ( - overrides?: Partial -): UserGeographyPopulation => ({ - type: 'geography', - userId: TEST_IDS.USER_ID, - geographyId: TEST_IDS.GEOGRAPHY_ID, - countryId: TEST_COUNTRIES.US, - scope: 'subnational', - label: TEST_LABELS.GEOGRAPHY, - createdAt: TEST_TIMESTAMPS.CREATED_AT, - ...overrides, -}); +// Note: UserGeographyPopulation removed - geographies are no longer stored as user associations // Mock UserPolicy export const createMockPolicyAssociation = (overrides?: Partial): UserPolicy => ({ diff --git a/app/src/tests/fixtures/hooks/hooksMocks.ts b/app/src/tests/fixtures/hooks/hooksMocks.ts index 66c622208..325f4e82d 100644 --- a/app/src/tests/fixtures/hooks/hooksMocks.ts +++ b/app/src/tests/fixtures/hooks/hooksMocks.ts @@ -1,10 +1,7 @@ import { QueryClient } from '@tanstack/react-query'; import { vi } from 'vitest'; import { CURRENT_YEAR } from '@/constants'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; +import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation'; import { HouseholdMetadata } from '@/types/metadata/householdMetadata'; import { HouseholdCreationPayload } from '@/types/payloads'; @@ -144,26 +141,7 @@ export const mockUserHouseholdPopulationList: UserHouseholdPopulation[] = [ }, ]; -export const mockUserGeographicAssociation: UserGeographyPopulation = { - type: 'geography', - id: TEST_IDS.GEOGRAPHY_ID, - userId: TEST_IDS.USER_ID, - countryId: GEO_CONSTANTS.COUNTRY_US, - scope: GEO_CONSTANTS.TYPE_SUBNATIONAL, - geographyId: GEO_CONSTANTS.REGION_CA, - label: TEST_LABELS.GEOGRAPHY, - createdAt: TEST_IDS.TIMESTAMP, -}; - -export const mockUserGeographicAssociationList: UserGeographyPopulation[] = [ - mockUserGeographicAssociation, - { - ...mockUserGeographicAssociation, - id: TEST_IDS.GEOGRAPHY_ID_2, - geographyId: GEO_CONSTANTS.REGION_NY, - label: TEST_LABELS.GEOGRAPHY_2, - }, -]; +// Note: UserGeographyPopulation mocks removed - geographies are no longer stored as user associations export const mockHouseholdCreationPayload: HouseholdCreationPayload = { country_id: GEO_CONSTANTS.COUNTRY_US, diff --git a/app/src/tests/fixtures/hooks/useFetchReportIngredientsMocks.ts b/app/src/tests/fixtures/hooks/useFetchReportIngredientsMocks.ts index 7b45e94e9..c18ffb613 100644 --- a/app/src/tests/fixtures/hooks/useFetchReportIngredientsMocks.ts +++ b/app/src/tests/fixtures/hooks/useFetchReportIngredientsMocks.ts @@ -48,6 +48,10 @@ export const TEST_USER_IDS = { /** * Basic society-wide report input (geography-based, no households) */ +/** + * Society-wide report input (geography-based) + * Note: Geographies are constructed from simulation data, not stored as user associations + */ export const SOCIETY_WIDE_INPUT: ReportIngredientsInput = { userReport: { id: TEST_IDS.REPORT.ID, @@ -68,15 +72,6 @@ export const SOCIETY_WIDE_INPUT: ReportIngredientsInput = { { policyId: TEST_IDS.POLICIES.REFORM, label: 'My Reform' }, ], userHouseholds: [], - userGeographies: [ - { - type: 'geography', - geographyId: TEST_IDS.GEOGRAPHIES.NATIONAL, - countryId: TEST_COUNTRIES.US, - scope: 'national', - label: 'United States', - }, - ], }; /** @@ -101,7 +96,6 @@ export const HOUSEHOLD_INPUT: ReportIngredientsInput = { label: 'My Household', }, ], - userGeographies: [], }; /** @@ -118,7 +112,6 @@ export const INPUT_WITHOUT_ID: ReportIngredientsInput = { ], userPolicies: [], userHouseholds: [], - userGeographies: [], }; /** @@ -133,7 +126,6 @@ export const MINIMAL_INPUT: ReportIngredientsInput = { userSimulations: [], userPolicies: [], userHouseholds: [], - userGeographies: [], }; // ============================================================================ @@ -177,16 +169,6 @@ export const createExpectedExpandedSocietyWide = (userId: string = TEST_USER_IDS }, ], userHouseholds: [], - userGeographies: [ - { - type: 'geography', - geographyId: TEST_IDS.GEOGRAPHIES.NATIONAL, - countryId: TEST_COUNTRIES.US, - scope: 'national', - label: 'United States', - userId, - }, - ], }); export const createExpectedExpandedWithoutId = (userId: string = TEST_USER_IDS.SHARED) => ({ @@ -207,7 +189,6 @@ export const createExpectedExpandedWithoutId = (userId: string = TEST_USER_IDS.S ], userPolicies: [], userHouseholds: [], - userGeographies: [], }); // ============================================================================ diff --git a/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts b/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts index 15ef2a74d..82737af06 100644 --- a/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts +++ b/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts @@ -46,15 +46,6 @@ export const MOCK_SAVE_SHARE_DATA: ReportIngredientsInput = { ], userPolicies: [{ policyId: TEST_IDS.POLICY, label: 'My Policy' }], userHouseholds: [], - userGeographies: [ - { - type: 'geography', - geographyId: TEST_IDS.GEOGRAPHY, - countryId: TEST_COUNTRIES.US, - scope: 'national', - label: 'United States', - }, - ], }; export const MOCK_SHARE_DATA_WITH_CURRENT_LAW: ReportIngredientsInput = { @@ -75,7 +66,6 @@ export const MOCK_SHARE_DATA_WITH_HOUSEHOLD: ReportIngredientsInput = { label: 'My Household', }, ], - userGeographies: [], }; export const MOCK_SHARE_DATA_WITHOUT_LABEL: ReportIngredientsInput = { @@ -87,7 +77,6 @@ export const MOCK_SHARE_DATA_WITHOUT_LABEL: ReportIngredientsInput = { userSimulations: [], userPolicies: [], userHouseholds: [], - userGeographies: [], }; // ============================================================================ diff --git a/app/src/tests/fixtures/hooks/useSharedReportDataMocks.tsx b/app/src/tests/fixtures/hooks/useSharedReportDataMocks.tsx index 2c0b27b3a..71b0bd3d5 100644 --- a/app/src/tests/fixtures/hooks/useSharedReportDataMocks.tsx +++ b/app/src/tests/fixtures/hooks/useSharedReportDataMocks.tsx @@ -20,15 +20,6 @@ export const MOCK_SHARE_DATA: ReportIngredientsInput = { userSimulations: [{ simulationId: 'sim-1', countryId: 'us', label: 'Baseline Sim' }], userPolicies: [{ policyId: 'policy-1', label: 'Test Policy' }], userHouseholds: [], - userGeographies: [ - { - type: 'geography', - geographyId: 'us', - countryId: 'us', - scope: 'national', - label: 'United States', - }, - ], }; export const MOCK_HOUSEHOLD_SHARE_DATA: ReportIngredientsInput = { @@ -43,7 +34,6 @@ export const MOCK_HOUSEHOLD_SHARE_DATA: ReportIngredientsInput = { userHouseholds: [ { type: 'household', householdId: 'hh-1', countryId: 'uk', label: 'My Household' }, ], - userGeographies: [], }; // API metadata fixtures (cast to any for test simplicity) diff --git a/app/src/tests/fixtures/hooks/useUserHouseholdMocks.ts b/app/src/tests/fixtures/hooks/useUserHouseholdMocks.ts index cc18a6b04..9cf217142 100644 --- a/app/src/tests/fixtures/hooks/useUserHouseholdMocks.ts +++ b/app/src/tests/fixtures/hooks/useUserHouseholdMocks.ts @@ -1,9 +1,7 @@ -// Fixtures for useUserHouseholds and useUserGeographics hooks +// Fixtures for useUserHouseholds hooks +// Note: useUserGeographics removed - geographies are no longer stored as user associations import { Geography } from '@/types/ingredients/Geography'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; +import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation'; import { HouseholdMetadata } from '@/types/metadata/householdMetadata'; // Test household IDs @@ -115,7 +113,9 @@ export const mockHouseholdMetadata2 = { isError: false, }; -// Mock geographic metadata +// Note: Geographic metadata mocks removed - geographies are no longer stored as user associations + +// Mock Geography objects (for use in simulations, not user associations) export const mockGeography1: Geography = { id: TEST_GEOGRAPHY_ID_1, countryId: 'us' as any, @@ -123,25 +123,6 @@ export const mockGeography1: Geography = { geographyId: 'us', }; -export const mockGeographyAssociation1: UserGeographyPopulation = { - id: 'association-3', - type: 'geography', - userId: 'user-123', - label: TEST_GEOGRAPHY_LABEL, - countryId: 'us', - scope: 'national', - geographyId: 'us', - createdAt: '2025-01-03T00:00:00Z', -}; - -export const mockGeographicMetadata = { - association: mockGeographyAssociation1, - geography: mockGeography1, - isLoading: false, - error: null, - isError: false, -}; - export const mockGeography2: Geography = { id: TEST_GEOGRAPHY_ID_2, countryId: 'us' as any, @@ -149,25 +130,6 @@ export const mockGeography2: Geography = { geographyId: 'ca', }; -export const mockGeographyAssociation2: UserGeographyPopulation = { - id: 'association-4', - type: 'geography', - userId: 'user-123', - label: 'California Population', - countryId: 'us', - scope: 'subnational', - geographyId: 'ca', - createdAt: '2025-01-04T00:00:00Z', -}; - -export const mockGeographicMetadata2 = { - association: mockGeographyAssociation2, - geography: mockGeography2, - isLoading: false, - error: null, - isError: false, -}; - // Mock hook return values export const mockUseUserHouseholdsSuccess = { data: [mockHouseholdMetadata, mockHouseholdMetadata2], @@ -193,26 +155,4 @@ export const mockUseUserHouseholdsEmpty = { associations: [], }; -export const mockUseUserGeographicsSuccess = { - data: [mockGeographicMetadata, mockGeographicMetadata2], - isLoading: false, - isError: false, - error: null, - associations: [mockGeographyAssociation1, mockGeographyAssociation2], -}; - -export const mockUseUserGeographicsLoading = { - data: undefined, - isLoading: true, - isError: false, - error: null, - associations: undefined, -}; - -export const mockUseUserGeographicsEmpty = { - data: [], - isLoading: false, - isError: false, - error: null, - associations: [], -}; +// Note: useUserGeographics mocks removed - geographies are no longer stored as user associations diff --git a/app/src/tests/fixtures/pages/populationsMocks.ts b/app/src/tests/fixtures/pages/populationsMocks.ts deleted file mode 100644 index 3d9b437bb..000000000 --- a/app/src/tests/fixtures/pages/populationsMocks.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { vi } from 'vitest'; -import { CURRENT_YEAR } from '@/constants'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; -import { HouseholdMetadata } from '@/types/metadata/householdMetadata'; - -// ============= TEST CONSTANTS ============= - -// IDs and identifiers -export const POPULATION_TEST_IDS = { - USER_ID: 'test-user-123', - HOUSEHOLD_ID_1: '1', - HOUSEHOLD_ID_2: '2', - GEOGRAPHIC_ID_1: '1', - GEOGRAPHIC_ID_2: '2', - TIMESTAMP_1: `${CURRENT_YEAR}-01-15T10:00:00Z`, - TIMESTAMP_2: `${CURRENT_YEAR}-01-20T14:30:00Z`, -} as const; - -// Labels and display text -export const POPULATION_LABELS = { - HOUSEHOLD_1: 'My Test Household', - HOUSEHOLD_2: `Family Household ${CURRENT_YEAR}`, - GEOGRAPHIC_1: 'California Population', - GEOGRAPHIC_2: 'United Kingdom National', - PAGE_TITLE: 'Your saved households', - PAGE_SUBTITLE: - 'Configure one or a collection of households to use in your simulation configurations.', - BUILD_BUTTON: 'New household(s)', - SEARCH_PLACEHOLDER: 'Search households', - MORE_FILTERS: 'More filters', - LOADING_TEXT: 'Loading...', - ERROR_TEXT: 'Error loading data', -} as const; - -// Geographic constants -export const POPULATION_GEO = { - COUNTRY_US: 'us', - COUNTRY_UK: 'uk', - STATE_CA: 'ca', - STATE_CA_LABEL: 'California', // Full label used when regions are loaded - STATE_NY: 'ny', - TYPE_NATIONAL: 'national' as const, - TYPE_SUBNATIONAL: 'subnational' as const, - REGION_TYPE_STATE: 'state' as const, - REGION_TYPE_CONSTITUENCY: 'constituency' as const, -} as const; - -// Menu actions -export const POPULATION_ACTIONS = { - VIEW_DETAILS: 'view-population', - BOOKMARK: 'bookmark', - EDIT: 'edit', - SHARE: 'share', - DELETE: 'delete', -} as const; - -// Action labels -export const POPULATION_ACTION_LABELS = { - VIEW_DETAILS: 'View details', - BOOKMARK: 'Bookmark', - EDIT: 'Edit', - SHARE: 'Share', - DELETE: 'Delete', -} as const; - -// Column headers -export const POPULATION_COLUMNS = { - NAME: 'Household name', - DATE: 'Date created', - DETAILS: 'Details', - CONNECTIONS: 'Connections', -} as const; - -// Detail text patterns -export const POPULATION_DETAILS = { - PERSON_SINGULAR: '1 person', - PERSON_PLURAL: (count: number) => `${count} persons`, - HOUSEHOLD_SINGULAR: '1 household', - HOUSEHOLD_PLURAL: (count: number) => `${count} households`, - NATIONAL: 'National', - SUBNATIONAL: 'Subnational', - STATE_PREFIX: 'State:', - CONSTITUENCY_PREFIX: 'Constituency:', - SAMPLE_SIMULATION: 'Sample simulation', - SAMPLE_REPORT: 'Sample report', - AVAILABLE_FOR_SIMULATIONS: 'Available for simulations', -} as const; - -// Console log messages -export const POPULATION_CONSOLE = { - MORE_FILTERS: 'More filters clicked', - VIEW_DETAILS: (id: string) => `View details: ${id}`, - BOOKMARK: (id: string) => `Bookmark population: ${id}`, - EDIT: (id: string) => `Edit population: ${id}`, - SHARE: (id: string) => `Share population: ${id}`, - DELETE: (id: string) => `Delete population: ${id}`, - UNKNOWN_ACTION: (action: string) => `Unknown action: ${action}`, -} as const; - -// Error messages -export const POPULATION_ERRORS = { - HOUSEHOLD_FETCH_FAILED: 'Failed to fetch household data', - GEOGRAPHIC_FETCH_FAILED: 'Failed to fetch geographic data', - COMBINED_ERROR: 'Error loading population data', -} as const; - -// ============= MOCK DATA OBJECTS ============= - -// Mock household metadata -export const mockHouseholdMetadata1: HouseholdMetadata = { - id: POPULATION_TEST_IDS.HOUSEHOLD_ID_1.split('-')[1], - country_id: POPULATION_GEO.COUNTRY_US, - household_json: { - people: { - person1: { - age: { [CURRENT_YEAR]: 30 }, - employment_income: { [CURRENT_YEAR]: 50000 }, - }, - person2: { - age: { [CURRENT_YEAR]: 28 }, - employment_income: { [CURRENT_YEAR]: 45000 }, - }, - }, - families: { - family1: { - members: ['person1', 'person2'], - }, - }, - tax_units: { - unit1: { - members: ['person1'], - }, - }, - spm_units: { - unit1: { - members: ['person1'], - }, - }, - households: { - household1: { - members: ['person1', 'person2'], - }, - }, - marital_units: { - unit1: { - members: ['person1', 'person2'], - }, - }, - }, - api_version: 'v1', - household_hash: '', -}; - -export const mockHouseholdMetadata2: HouseholdMetadata = { - id: POPULATION_TEST_IDS.HOUSEHOLD_ID_2.split('-')[1], - country_id: POPULATION_GEO.COUNTRY_US, - household_json: { - people: { - person1: { - age: { [CURRENT_YEAR]: 45 }, - }, - }, - families: {}, - tax_units: { - unit1: { - members: ['person1'], - }, - }, - spm_units: { - unit1: { - members: ['person1'], - }, - }, - households: { - household1: { - members: ['person1', 'person2'], - }, - }, - marital_units: { - unit1: { - members: ['person1', 'person2'], - }, - }, - }, - api_version: 'v1', - household_hash: '', -}; - -// Mock household associations -export const mockHouseholdAssociation1: UserHouseholdPopulation = { - type: 'household', - id: POPULATION_TEST_IDS.HOUSEHOLD_ID_1, - householdId: POPULATION_TEST_IDS.HOUSEHOLD_ID_1, - userId: POPULATION_TEST_IDS.USER_ID, - countryId: POPULATION_GEO.COUNTRY_US, - label: POPULATION_LABELS.HOUSEHOLD_1, - createdAt: POPULATION_TEST_IDS.TIMESTAMP_1, - updatedAt: POPULATION_TEST_IDS.TIMESTAMP_1, - isCreated: true, -}; - -export const mockHouseholdAssociation2: UserHouseholdPopulation = { - type: 'household', - id: POPULATION_TEST_IDS.HOUSEHOLD_ID_2, - householdId: POPULATION_TEST_IDS.HOUSEHOLD_ID_2, - userId: POPULATION_TEST_IDS.USER_ID, - countryId: POPULATION_GEO.COUNTRY_US, - label: POPULATION_LABELS.HOUSEHOLD_2, - createdAt: POPULATION_TEST_IDS.TIMESTAMP_2, - updatedAt: POPULATION_TEST_IDS.TIMESTAMP_2, - isCreated: true, -}; - -// Mock geographic associations -export const mockGeographicAssociation1: UserGeographyPopulation = { - type: 'geography', - id: POPULATION_TEST_IDS.GEOGRAPHIC_ID_1, - userId: POPULATION_TEST_IDS.USER_ID, - countryId: POPULATION_GEO.COUNTRY_US, - scope: POPULATION_GEO.TYPE_SUBNATIONAL, - geographyId: POPULATION_GEO.STATE_CA, - label: POPULATION_LABELS.GEOGRAPHIC_1, - createdAt: POPULATION_TEST_IDS.TIMESTAMP_1, -}; - -export const mockGeographicAssociation2: UserGeographyPopulation = { - type: 'geography', - id: POPULATION_TEST_IDS.GEOGRAPHIC_ID_2, - userId: POPULATION_TEST_IDS.USER_ID, - countryId: POPULATION_GEO.COUNTRY_UK, - scope: POPULATION_GEO.TYPE_NATIONAL, - geographyId: POPULATION_GEO.COUNTRY_UK, - label: POPULATION_LABELS.GEOGRAPHIC_2, - createdAt: POPULATION_TEST_IDS.TIMESTAMP_2, -}; - -// Combined mock data for useUserHouseholds hook -export const mockUserHouseholdsData = [ - { - association: mockHouseholdAssociation1, - household: mockHouseholdMetadata1, - isLoading: false, - error: null, - }, - { - association: mockHouseholdAssociation2, - household: mockHouseholdMetadata2, - isLoading: false, - error: null, - }, -]; - -export const mockGeographicAssociationsData = [ - mockGeographicAssociation1, - mockGeographicAssociation2, -]; - -// ============= MOCK FUNCTIONS ============= - -// Redux dispatch mock -export const mockDispatch = vi.fn(); - -// Hook mocks -export const mockUseUserHouseholds = vi.fn(() => ({ - data: mockUserHouseholdsData, - isLoading: false, - isError: false, - error: null, -})); - -export const mockUseGeographicAssociationsByUser = vi.fn(() => ({ - data: mockGeographicAssociationsData, - isLoading: false, - isError: false, - error: null, -})); - -// ============= TEST HELPERS ============= - -export const setupMockConsole = () => { - const consoleSpy = { - log: vi.spyOn(console, 'log').mockImplementation(() => {}), - error: vi.spyOn(console, 'error').mockImplementation(() => {}), - warn: vi.spyOn(console, 'warn').mockImplementation(() => {}), - }; - - return { - consoleSpy, - restore: () => { - consoleSpy.log.mockRestore(); - consoleSpy.error.mockRestore(); - consoleSpy.warn.mockRestore(); - }, - }; -}; - -// Helper to create loading states -export const createLoadingState = (householdLoading = true, geographicLoading = false) => ({ - household: { - data: householdLoading ? undefined : mockUserHouseholdsData, - isLoading: householdLoading, - isError: false, - error: null, - }, - geographic: { - data: geographicLoading ? undefined : mockGeographicAssociationsData, - isLoading: geographicLoading, - isError: false, - error: null, - }, -}); - -// Helper to create error states -export const createErrorState = (householdError = false, geographicError = false) => ({ - household: { - data: householdError ? undefined : mockUserHouseholdsData, - isLoading: false, - isError: householdError, - error: householdError ? new Error(POPULATION_ERRORS.HOUSEHOLD_FETCH_FAILED) : null, - }, - geographic: { - data: geographicError ? undefined : mockGeographicAssociationsData, - isLoading: false, - isError: geographicError, - error: geographicError ? new Error(POPULATION_ERRORS.GEOGRAPHIC_FETCH_FAILED) : null, - }, -}); - -// Helper to create empty data states -export const createEmptyDataState = () => ({ - household: { - data: [], - isLoading: false, - isError: false, - error: null, - }, - geographic: { - data: [], - isLoading: false, - isError: false, - error: null, - }, -}); diff --git a/app/src/tests/fixtures/utils/populationMatchingMocks.ts b/app/src/tests/fixtures/utils/populationMatchingMocks.ts deleted file mode 100644 index 5aace9d0a..000000000 --- a/app/src/tests/fixtures/utils/populationMatchingMocks.ts +++ /dev/null @@ -1,189 +0,0 @@ -import type { UserGeographicMetadataWithAssociation } from '@/hooks/useUserGeographic'; -import type { UserHouseholdMetadataWithAssociation } from '@/hooks/useUserHousehold'; -import type { Simulation } from '@/types/ingredients/Simulation'; - -/** - * Test constants for populationMatching utility - */ - -export const TEST_HOUSEHOLD_IDS = { - HOUSEHOLD_123: 'hh-123', - HOUSEHOLD_456: 'hh-456', - NON_EXISTENT: 'hh-999', - // Type mismatch test cases - simulating API returning numeric IDs - NUMERIC_STRING_MATCH: '56324', // String version - NUMERIC_VALUE: 56324, // Numeric version (simulates API bug) -} as const; - -export const TEST_GEOGRAPHY_IDS = { - CALIFORNIA: 'geo-abc', - LONDON: 'geo-xyz', - NON_EXISTENT: 'geo-999', - // Type mismatch test cases - simulating API returning numeric IDs - NUMERIC_STRING_MATCH: '12345', // String version - NUMERIC_VALUE: 12345, // Numeric version (simulates API bug) -} as const; - -export const TEST_SIMULATION_ID = 'sim-1'; -export const TEST_USER_ID = '1'; - -/** - * Mock household population data for testing - */ -export const mockHouseholdData: UserHouseholdMetadataWithAssociation[] = [ - { - association: { - id: 'user-hh-1', - userId: TEST_USER_ID, - householdId: TEST_HOUSEHOLD_IDS.HOUSEHOLD_123, - countryId: 'us', - type: 'household', - }, - household: { - id: TEST_HOUSEHOLD_IDS.HOUSEHOLD_123, - country_id: 'us', - api_version: '1.0.0', - household_json: '{}' as any, - household_hash: 'hash123', - }, - isLoading: false, - error: null, - }, - { - association: { - id: 'user-hh-2', - userId: TEST_USER_ID, - householdId: TEST_HOUSEHOLD_IDS.HOUSEHOLD_456, - countryId: 'us', - type: 'household', - }, - household: { - id: TEST_HOUSEHOLD_IDS.HOUSEHOLD_456, - country_id: 'us', - api_version: '1.0.0', - household_json: '{}' as any, - household_hash: 'hash456', - }, - isLoading: false, - error: null, - }, -]; - -/** - * Mock geographic population data for testing - */ -export const mockGeographicData: UserGeographicMetadataWithAssociation[] = [ - { - association: { - id: 'user-geo-1', - userId: TEST_USER_ID, - geographyId: TEST_GEOGRAPHY_IDS.CALIFORNIA, - countryId: 'us', - type: 'geography', - scope: 'national', - }, - geography: { - id: TEST_GEOGRAPHY_IDS.CALIFORNIA, - name: 'California', - countryId: 'us', - scope: 'national', - geographyId: 'ca', - }, - isLoading: false, - error: null, - }, - { - association: { - id: 'user-geo-2', - userId: TEST_USER_ID, - geographyId: TEST_GEOGRAPHY_IDS.LONDON, - countryId: 'uk', - type: 'geography', - scope: 'national', - }, - geography: { - id: TEST_GEOGRAPHY_IDS.LONDON, - name: 'London', - countryId: 'uk', - scope: 'national', - geographyId: 'london', - }, - isLoading: false, - error: null, - }, -]; - -/** - * Mock household data for type mismatch testing (numeric populationId vs string household.id) - * Simulates the production bug where API returns numeric IDs - */ -export const mockHouseholdDataWithNumericMismatch: UserHouseholdMetadataWithAssociation[] = [ - { - association: { - userId: 'test-user', - householdId: TEST_HOUSEHOLD_IDS.NUMERIC_STRING_MATCH, - countryId: 'uk', - label: 'Test Household', - type: 'household', - createdAt: new Date().toISOString(), - }, - household: { - id: TEST_HOUSEHOLD_IDS.NUMERIC_STRING_MATCH, // String ID - country_id: 'uk', - api_version: '2.39.0', - household_json: { - people: {}, - families: {}, - tax_units: {}, - spm_units: {}, - households: {}, - marital_units: {}, - }, - household_hash: 'test-hash', - }, - isLoading: false, - error: null, - isError: false, - }, -]; - -/** - * Mock geographic data for type mismatch testing (numeric populationId vs string geography.id) - * Simulates the production bug where API returns numeric IDs - */ -export const mockGeographicDataWithNumericMismatch: UserGeographicMetadataWithAssociation[] = [ - { - association: { - userId: 'test-user', - geographyId: TEST_GEOGRAPHY_IDS.NUMERIC_STRING_MATCH, - countryId: 'us', - scope: 'subnational', - label: 'Test Region', - type: 'geography', - createdAt: new Date().toISOString(), - }, - geography: { - id: TEST_GEOGRAPHY_IDS.NUMERIC_STRING_MATCH, // String ID - countryId: 'us', - scope: 'subnational', - geographyId: TEST_GEOGRAPHY_IDS.NUMERIC_STRING_MATCH, - name: 'Test Region', - }, - isLoading: false, - error: null, - isError: false, - }, -]; - -/** - * Helper to create a mock simulation - */ -export function createMockSimulation(overrides: Partial = {}): Simulation { - return { - id: TEST_SIMULATION_ID, - countryId: 'us', - policyId: 'policy-1', - populationType: 'household', - ...overrides, - } as Simulation; -} diff --git a/app/src/tests/fixtures/utils/populationOpsMocks.ts b/app/src/tests/fixtures/utils/populationOpsMocks.ts deleted file mode 100644 index 203db5918..000000000 --- a/app/src/tests/fixtures/utils/populationOpsMocks.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { vi } from 'vitest'; -import { countryIds } from '@/libs/countries'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; -import { GeographyPopulationRef, HouseholdPopulationRef } from '@/utils/PopulationOps'; - -// ============= TEST CONSTANTS ============= - -// IDs -export const POPULATION_IDS = { - HOUSEHOLD_1: '123', - HOUSEHOLD_2: '456', - HOUSEHOLD_EMPTY: '', - GEOGRAPHY_1: 'us-ca', - GEOGRAPHY_2: 'uk-england', - GEOGRAPHY_EMPTY: '', - USER_1: 'user-123', - USER_2: 'user-456', -} as const; - -// Labels -export const POPULATION_LABELS = { - CUSTOM_LABEL: 'My Custom Population', - HOUSEHOLD_LABEL: 'My Household Configuration', - GEOGRAPHY_LABEL: 'California State', -} as const; - -// Countries -export const POPULATION_COUNTRIES = { - US: 'us', - UK: 'uk', - CA: 'ca', -} as const; - -// Scopes -export const POPULATION_SCOPES = { - NATIONAL: 'national', - SUBNATIONAL: 'subnational', -} as const; - -// Expected strings -export const EXPECTED_LABELS = { - HOUSEHOLD_DEFAULT: (id: string) => `Household ${id}`, - GEOGRAPHY_DEFAULT: (id: string) => `All households in ${id}`, - HOUSEHOLD_TYPE: 'Household', - GEOGRAPHY_TYPE: 'Household collection', - NATIONAL_PREFIX: 'National households', - REGIONAL_PREFIX: 'Households in', -} as const; - -// Cache keys -export const EXPECTED_CACHE_KEYS = { - HOUSEHOLD: (id: string) => `household:${id}`, - GEOGRAPHY: (id: string) => `geography:${id}`, -} as const; - -// API payload keys -export const API_PAYLOAD_KEYS = { - POPULATION_ID: 'population_id', - HOUSEHOLD_ID: 'household_id', - GEOGRAPHY_ID: 'geography_id', - REGION: 'region', -} as const; - -// ============= MOCK DATA OBJECTS ============= - -// Household population references -export const mockHouseholdPopRef1: HouseholdPopulationRef = { - type: 'household', - householdId: POPULATION_IDS.HOUSEHOLD_1, -}; - -export const mockHouseholdPopRef2: HouseholdPopulationRef = { - type: 'household', - householdId: POPULATION_IDS.HOUSEHOLD_2, -}; - -export const mockHouseholdPopRefEmpty: HouseholdPopulationRef = { - type: 'household', - householdId: POPULATION_IDS.HOUSEHOLD_EMPTY, -}; - -// Geography population references -export const mockGeographyPopRef1: GeographyPopulationRef = { - type: 'geography', - geographyId: POPULATION_IDS.GEOGRAPHY_1, -}; - -export const mockGeographyPopRef2: GeographyPopulationRef = { - type: 'geography', - geographyId: POPULATION_IDS.GEOGRAPHY_2, -}; - -export const mockGeographyPopRefEmpty: GeographyPopulationRef = { - type: 'geography', - geographyId: POPULATION_IDS.GEOGRAPHY_EMPTY, -}; - -// User household populations -export const mockUserHouseholdPop: UserHouseholdPopulation = { - type: 'household', - householdId: POPULATION_IDS.HOUSEHOLD_1, - userId: POPULATION_IDS.USER_1, - countryId: 'us', - label: POPULATION_LABELS.HOUSEHOLD_LABEL, - isCreated: true, -}; - -export const mockUserHouseholdPopNoLabel: UserHouseholdPopulation = { - type: 'household', - householdId: POPULATION_IDS.HOUSEHOLD_2, - userId: POPULATION_IDS.USER_2, - countryId: 'us', -}; - -export const mockUserHouseholdPopInvalid: UserHouseholdPopulation = { - type: 'household', - householdId: POPULATION_IDS.HOUSEHOLD_EMPTY, - userId: POPULATION_IDS.USER_1, - countryId: 'us', -}; - -export const mockUserHouseholdPopNoUser: UserHouseholdPopulation = { - type: 'household', - householdId: POPULATION_IDS.HOUSEHOLD_1, - userId: '', - countryId: 'us', -}; - -// User geography populations -export const mockUserGeographyPop: UserGeographyPopulation = { - type: 'geography', - geographyId: POPULATION_IDS.GEOGRAPHY_1, - countryId: POPULATION_COUNTRIES.US, - scope: POPULATION_SCOPES.SUBNATIONAL as any, - userId: POPULATION_IDS.USER_1, - label: POPULATION_LABELS.GEOGRAPHY_LABEL, -}; - -export const mockUserGeographyPopNational: UserGeographyPopulation = { - type: 'geography', - geographyId: POPULATION_IDS.GEOGRAPHY_2, - countryId: POPULATION_COUNTRIES.UK, - scope: POPULATION_SCOPES.NATIONAL as any, - userId: POPULATION_IDS.USER_2, -}; - -export const mockUserGeographyPopInvalid: UserGeographyPopulation = { - type: 'geography', - geographyId: POPULATION_IDS.GEOGRAPHY_EMPTY, - countryId: POPULATION_COUNTRIES.US, - scope: POPULATION_SCOPES.NATIONAL as any, - userId: POPULATION_IDS.USER_1, -}; - -// ============= EXPECTED RESULTS ============= - -// Expected API payloads -export const expectedHouseholdAPIPayload = { - [API_PAYLOAD_KEYS.POPULATION_ID]: POPULATION_IDS.HOUSEHOLD_1, - [API_PAYLOAD_KEYS.HOUSEHOLD_ID]: POPULATION_IDS.HOUSEHOLD_1, -}; - -export const expectedGeographyAPIPayload = { - [API_PAYLOAD_KEYS.GEOGRAPHY_ID]: POPULATION_IDS.GEOGRAPHY_1, - [API_PAYLOAD_KEYS.REGION]: POPULATION_IDS.GEOGRAPHY_1, -}; - -// Expected labels -export const expectedHouseholdLabel = EXPECTED_LABELS.HOUSEHOLD_DEFAULT(POPULATION_IDS.HOUSEHOLD_1); -export const expectedGeographyLabel = EXPECTED_LABELS.GEOGRAPHY_DEFAULT(POPULATION_IDS.GEOGRAPHY_1); - -// Expected cache keys -export const expectedHouseholdCacheKey = EXPECTED_CACHE_KEYS.HOUSEHOLD(POPULATION_IDS.HOUSEHOLD_1); -export const expectedGeographyCacheKey = EXPECTED_CACHE_KEYS.GEOGRAPHY(POPULATION_IDS.GEOGRAPHY_1); - -// Expected user population labels -export const expectedUserHouseholdLabel = POPULATION_LABELS.HOUSEHOLD_LABEL; -export const expectedUserHouseholdDefaultLabel = EXPECTED_LABELS.HOUSEHOLD_DEFAULT( - POPULATION_IDS.HOUSEHOLD_2 -); -export const expectedUserGeographyLabel = POPULATION_LABELS.GEOGRAPHY_LABEL; -export const expectedUserGeographyNationalLabel = `${EXPECTED_LABELS.NATIONAL_PREFIX} ${POPULATION_IDS.GEOGRAPHY_2}`; -export const expectedUserGeographyRegionalLabel = `${EXPECTED_LABELS.REGIONAL_PREFIX} ${POPULATION_IDS.GEOGRAPHY_1}`; - -// ============= TEST HELPERS ============= - -// Helper to create a household population ref -export const createHouseholdPopRef = (householdId: string): HouseholdPopulationRef => ({ - type: 'household', - householdId, -}); - -// Helper to create a geography population ref -export const createGeographyPopRef = (geographyId: string): GeographyPopulationRef => ({ - type: 'geography', - geographyId, -}); - -// Helper to create a user household population -export const createUserHouseholdPop = ( - householdId: string, - userId: string, - label?: string -): UserHouseholdPopulation => ({ - type: 'household', - householdId, - userId, - countryId: 'us', - ...(label && { label }), -}); - -// Helper to create a user geography population -export const createUserGeographyPop = ( - geographyId: string, - countryId: (typeof countryIds)[number], - scope: 'national' | 'subnational', - userId: string, - label?: string -): UserGeographyPopulation => ({ - type: 'geography', - geographyId, - countryId, - scope, - userId, - ...(label && { label }), -}); - -// Helper to verify API payload -export const verifyAPIPayload = ( - payload: Record, - expectedKeys: string[], - expectedValues: Record -): void => { - expectedKeys.forEach((key) => { - expect(payload).toHaveProperty(key); - expect(payload[key]).toBe(expectedValues[key]); - }); -}; - -// Mock handler functions for testing pattern matching -export const mockHandlers = { - household: vi.fn(), - geography: vi.fn(), -}; - -// Helper to reset mock handlers -export const resetMockHandlers = (): void => { - mockHandlers.household.mockReset(); - mockHandlers.geography.mockReset(); -}; - -// Helper to setup mock handler returns -export const setupMockHandlerReturns = (householdReturn: T, geographyReturn: T): void => { - mockHandlers.household.mockReturnValue(householdReturn); - mockHandlers.geography.mockReturnValue(geographyReturn); -}; diff --git a/app/src/tests/fixtures/utils/shareUtilsMocks.ts b/app/src/tests/fixtures/utils/shareUtilsMocks.ts deleted file mode 100644 index 3ef23932a..000000000 --- a/app/src/tests/fixtures/utils/shareUtilsMocks.ts +++ /dev/null @@ -1,198 +0,0 @@ -/** - * Test fixtures for shareUtils tests - */ - -import { ReportIngredientsInput } from '@/hooks/utils/useFetchReportIngredients'; -import { UserPolicy } from '@/types/ingredients/UserPolicy'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; -import { UserReport } from '@/types/ingredients/UserReport'; -import { UserSimulation } from '@/types/ingredients/UserSimulation'; - -// ============================================================================ -// Constants -// ============================================================================ - -export const TEST_USER_REPORT_IDS = { - SOCIETY_WIDE: 'sur-abc123', - HOUSEHOLD: 'sur-def456', - TEST: 'sur-test1', -} as const; - -export const TEST_BASE_REPORT_IDS = { - SOCIETY_WIDE: '308', - HOUSEHOLD: '309', - TEST: '100', -} as const; - -export const TEST_COUNTRIES = { - US: 'us', - UK: 'uk', -} as const; - -// ============================================================================ -// ReportIngredientsInput Fixtures - Society-wide report (geographies, no households) -// ============================================================================ - -export const VALID_SHARE_DATA: ReportIngredientsInput = { - userReport: { - id: TEST_USER_REPORT_IDS.SOCIETY_WIDE, - reportId: TEST_BASE_REPORT_IDS.SOCIETY_WIDE, - countryId: TEST_COUNTRIES.US, - label: 'My Report', - }, - userSimulations: [ - { simulationId: 'sim-1', countryId: TEST_COUNTRIES.US, label: 'Baseline' }, - { simulationId: 'sim-2', countryId: TEST_COUNTRIES.US, label: 'Reform' }, - ], - userPolicies: [ - { policyId: 'policy-1', label: 'Current Law' }, - { policyId: 'policy-2', label: 'My Policy' }, - ], - userHouseholds: [], - userGeographies: [ - { - type: 'geography', - geographyId: TEST_COUNTRIES.US, - countryId: TEST_COUNTRIES.US, - scope: 'national', - label: 'United States', - }, - ], -}; - -// ============================================================================ -// ReportIngredientsInput Fixtures - Household report (households, no geographies) -// ============================================================================ - -export const VALID_HOUSEHOLD_SHARE_DATA: ReportIngredientsInput = { - userReport: { - id: TEST_USER_REPORT_IDS.HOUSEHOLD, - reportId: TEST_BASE_REPORT_IDS.HOUSEHOLD, - countryId: TEST_COUNTRIES.UK, - label: 'Household Report', - }, - userSimulations: [ - { simulationId: 'sim-3', countryId: TEST_COUNTRIES.UK, label: 'My Simulation' }, - ], - userPolicies: [{ policyId: 'policy-3', label: 'My Policy' }], - userHouseholds: [ - { - type: 'household', - householdId: 'household-123', - countryId: TEST_COUNTRIES.UK, - label: 'My Household', - }, - ], - userGeographies: [], -}; - -// ============================================================================ -// Full UserReport/UserSimulation/etc. fixtures (with userId for createShareData tests) -// ============================================================================ - -export const MOCK_USER_REPORT: UserReport = { - id: TEST_USER_REPORT_IDS.TEST, - userId: 'anonymous', - reportId: TEST_BASE_REPORT_IDS.TEST, - countryId: TEST_COUNTRIES.US, - label: 'My Report', -}; - -export const MOCK_USER_SIMULATIONS: UserSimulation[] = [ - { - userId: 'anonymous', - simulationId: 'sim-1', - countryId: TEST_COUNTRIES.US, - label: 'Sim Label', - }, -]; - -export const MOCK_USER_POLICIES: UserPolicy[] = [ - { - userId: 'anonymous', - policyId: 'policy-1', - label: 'Policy Label', - }, -]; - -export const MOCK_USER_GEOGRAPHIES: UserGeographyPopulation[] = [ - { - type: 'geography', - userId: 'anonymous', - geographyId: 'geo-1', - countryId: TEST_COUNTRIES.US, - scope: 'national', - label: 'Geography Label', - }, -]; - -export const MOCK_USER_HOUSEHOLDS: UserHouseholdPopulation[] = []; - -// ============================================================================ -// Invalid data fixtures for validation tests -// ============================================================================ - -export const createInvalidShareDataMissingUserReport = () => ({ - ...VALID_SHARE_DATA, - userReport: undefined, -}); - -export const createInvalidShareDataNonArraySimulations = () => ({ - ...VALID_SHARE_DATA, - userSimulations: 'not-an-array', -}); - -export const createInvalidShareDataNullSimulationId = () => ({ - ...VALID_SHARE_DATA, - userSimulations: [{ simulationId: null, countryId: TEST_COUNTRIES.US }], -}); - -export const createInvalidShareDataBadCountryId = () => ({ - ...VALID_SHARE_DATA, - userReport: { ...VALID_SHARE_DATA.userReport, countryId: 'invalid' }, -}); - -export const createInvalidShareDataBadGeographyScope = () => ({ - ...VALID_SHARE_DATA, - userGeographies: [ - { geographyId: TEST_COUNTRIES.US, countryId: TEST_COUNTRIES.US, scope: 'invalid' }, - ], -}); - -export const createShareDataWithoutId = () => - ({ - ...VALID_SHARE_DATA, - userReport: { - ...VALID_SHARE_DATA.userReport, - id: undefined, - }, - }) as unknown as ReportIngredientsInput; - -// ============================================================================ -// Helper functions -// ============================================================================ - -/** - * Create a UserReport without id field for testing null returns - */ -export const createUserReportWithoutId = (): UserReport => - ({ - id: undefined as unknown as string, - userId: 'anonymous', - reportId: TEST_BASE_REPORT_IDS.TEST, - countryId: TEST_COUNTRIES.US, - }) as UserReport; - -/** - * Create a UserReport without reportId field for testing null returns - */ -export const createUserReportWithoutReportId = (): UserReport => - ({ - id: TEST_USER_REPORT_IDS.TEST, - userId: 'anonymous', - reportId: undefined as unknown as string, - countryId: TEST_COUNTRIES.US, - }) as UserReport; diff --git a/app/src/tests/unit/api/geographicAssociation.test.ts b/app/src/tests/unit/api/geographicAssociation.test.ts deleted file mode 100644 index 98a94753f..000000000 --- a/app/src/tests/unit/api/geographicAssociation.test.ts +++ /dev/null @@ -1,479 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { ApiGeographicStore, LocalStorageGeographicStore } from '@/api/geographicAssociation'; -import type { UserGeographyPopulation } from '@/types/ingredients/UserPopulation'; - -// Mock fetch -global.fetch = vi.fn(); - -describe('ApiGeographicStore', () => { - let store: ApiGeographicStore; - - const mockPopulation: UserGeographyPopulation = { - userId: 'user-123', - geographyId: 'geo-456', - countryId: 'us', - label: 'Test Geography', - type: 'geography', - scope: 'subnational', - createdAt: '2025-01-01T00:00:00Z', - }; - - const mockApiResponse = { - user_id: 'user-123', - geography_id: 'geo-456', - country_id: 'us', - label: 'Test Geography', - scope: 'subnational', - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }; - - beforeEach(() => { - store = new ApiGeographicStore(); - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - describe('create', () => { - it('given valid population then creates geographic association', async () => { - // Given - (global.fetch as any).mockResolvedValue({ - ok: true, - json: async () => mockApiResponse, - }); - - // When - const result = await store.create(mockPopulation); - - // Then - expect(fetch).toHaveBeenCalledWith( - '/api/user-geographic-associations', - expect.objectContaining({ - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }) - ); - expect(result).toMatchObject({ - userId: 'user-123', - geographyId: 'geo-456', - countryId: 'us', - label: 'Test Geography', - scope: 'subnational', - }); - }); - - it('given API error then throws error', async () => { - // Given - (global.fetch as any).mockResolvedValue({ - ok: false, - status: 500, - }); - - // When/Then - await expect(store.create(mockPopulation)).rejects.toThrow( - 'Failed to create geographic association' - ); - }); - }); - - describe('findByUser', () => { - it('given valid user ID then fetches user associations', async () => { - // Given - (global.fetch as any).mockResolvedValue({ - ok: true, - json: async () => [mockApiResponse], - }); - - // When - const result = await store.findByUser('user-123'); - - // Then - expect(fetch).toHaveBeenCalledWith( - '/api/user-geographic-associations/user/user-123', - expect.objectContaining({ - headers: { 'Content-Type': 'application/json' }, - }) - ); - expect(result).toHaveLength(1); - expect(result[0]).toMatchObject({ - userId: 'user-123', - geographyId: 'geo-456', - countryId: 'us', - label: 'Test Geography', - scope: 'subnational', - }); - }); - - it('given API error then throws error', async () => { - // Given - (global.fetch as any).mockResolvedValue({ - ok: false, - status: 500, - }); - - // When/Then - await expect(store.findByUser('user-123')).rejects.toThrow( - 'Failed to fetch user associations' - ); - }); - }); - - describe('findById', () => { - it('given valid IDs then fetches specific association', async () => { - // Given - (global.fetch as any).mockResolvedValue({ - ok: true, - status: 200, - json: async () => mockApiResponse, - }); - - // When - const result = await store.findById('user-123', 'geo-456'); - - // Then - expect(fetch).toHaveBeenCalledWith( - '/api/user-geographic-associations/user-123/geo-456', - expect.objectContaining({ - headers: { 'Content-Type': 'application/json' }, - }) - ); - expect(result).toMatchObject({ - userId: 'user-123', - geographyId: 'geo-456', - countryId: 'us', - label: 'Test Geography', - scope: 'subnational', - }); - }); - - it('given 404 response then returns null', async () => { - // Given - (global.fetch as any).mockResolvedValue({ - ok: false, - status: 404, - }); - - // When - const result = await store.findById('user-123', 'nonexistent'); - - // Then - expect(result).toBeNull(); - }); - - it('given other error then throws error', async () => { - // Given - (global.fetch as any).mockResolvedValue({ - ok: false, - status: 500, - }); - - // When/Then - await expect(store.findById('user-123', 'geo-456')).rejects.toThrow( - 'Failed to fetch association' - ); - }); - }); - - describe('update', () => { - it('given update called then throws not supported error', async () => { - // Given & When & Then - await expect(store.update('user-123', 'geo-456', { label: 'Updated Label' })).rejects.toThrow( - 'Please ensure you are using localStorage mode' - ); - }); - - it('given update called then logs warning', async () => { - // Given - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - // When - try { - await store.update('user-123', 'geo-456', { label: 'Updated Label' }); - } catch { - // Expected to throw - } - - // Then - expect(consoleWarnSpy).toHaveBeenCalledWith( - expect.stringContaining('API endpoint not yet implemented') - ); - - consoleWarnSpy.mockRestore(); - }); - }); -}); - -describe('LocalStorageGeographicStore', () => { - let store: LocalStorageGeographicStore; - let mockLocalStorage: { [key: string]: string }; - - const mockPopulation1: UserGeographyPopulation = { - userId: 'user-123', - geographyId: 'geo-456', - countryId: 'us', - label: 'Test Geography 1', - type: 'geography', - scope: 'subnational', - id: 'geo-456', - createdAt: '2025-01-01T00:00:00Z', - isCreated: true, - }; - - const mockPopulation2: UserGeographyPopulation = { - userId: 'user-123', - geographyId: 'geo-789', - countryId: 'uk', - label: 'Test Geography 2', - type: 'geography', - scope: 'subnational', - id: 'geo-789', - createdAt: '2025-01-02T00:00:00Z', - isCreated: true, - }; - - beforeEach(() => { - // Mock localStorage - mockLocalStorage = {}; - global.localStorage = { - getItem: vi.fn((key) => mockLocalStorage[key] || null), - setItem: vi.fn((key, value) => { - mockLocalStorage[key] = value; - }), - removeItem: vi.fn((key) => { - delete mockLocalStorage[key]; - }), - clear: vi.fn(() => { - mockLocalStorage = {}; - }), - length: 0, - key: vi.fn(), - }; - - store = new LocalStorageGeographicStore(); - vi.clearAllMocks(); - }); - - describe('create', () => { - it('given new population then stores in localStorage', async () => { - // When - const result = await store.create(mockPopulation1); - - // Then - expect(result).toMatchObject({ - userId: 'user-123', - geographyId: 'geo-456', - countryId: 'us', - label: 'Test Geography 1', - }); - expect(result.createdAt).toBeDefined(); - expect(localStorage.setItem).toHaveBeenCalled(); - }); - - it('given population without createdAt then adds timestamp', async () => { - // Given - const populationWithoutDate = { ...mockPopulation1, createdAt: undefined }; - - // When - const result = await store.create(populationWithoutDate as any); - - // Then - expect(result.createdAt).toBeDefined(); - }); - - it('given duplicate population then allows creation', async () => { - // Given - await store.create(mockPopulation1); - - // When - const result = await store.create(mockPopulation1); - - // Then - Implementation allows duplicates for multiple entries of same geography - expect(result).toEqual(mockPopulation1); - const allPopulations = await store.findByUser('user-123'); - expect(allPopulations).toHaveLength(2); - }); - }); - - describe('findByUser', () => { - it('given user with populations then returns all user populations', async () => { - // Given - await store.create(mockPopulation1); - await store.create(mockPopulation2); - - // When - const result = await store.findByUser('user-123'); - - // Then - expect(result).toHaveLength(2); - expect(result[0].geographyId).toBe('geo-456'); - expect(result[1].geographyId).toBe('geo-789'); - }); - - it('given user with no populations then returns empty array', async () => { - // When - const result = await store.findByUser('nonexistent-user'); - - // Then - expect(result).toEqual([]); - }); - }); - - describe('findById', () => { - it('given existing population then returns it', async () => { - // Given - await store.create(mockPopulation1); - - // When - const result = await store.findById('user-123', 'geo-456'); - - // Then - expect(result).toMatchObject({ - userId: 'user-123', - geographyId: 'geo-456', - countryId: 'us', - }); - }); - - it('given nonexistent population then returns null', async () => { - // When - const result = await store.findById('user-123', 'nonexistent'); - - // Then - expect(result).toBeNull(); - }); - }); - - describe('update', () => { - it('given existing geography then update succeeds and returns updated geography', async () => { - // Given - await store.create(mockPopulation1); - - // When - const result = await store.update('user-123', 'geo-456', { label: 'Updated Label' }); - - // Then - expect(result.label).toBe('Updated Label'); - expect(result.userId).toBe('user-123'); - expect(result.geographyId).toBe('geo-456'); - expect(result.updatedAt).toBeDefined(); - }); - - it('given nonexistent geography then update throws error', async () => { - // Given - no geography created - - // When & Then - await expect( - store.update('user-123', 'nonexistent', { label: 'Updated Label' }) - ).rejects.toThrow('UserGeography with userId user-123 and geographyId nonexistent not found'); - }); - - it('given existing geography then updatedAt timestamp is set', async () => { - // Given - await store.create(mockPopulation1); - const beforeUpdate = new Date().toISOString(); - - // When - const result = await store.update('user-123', 'geo-456', { label: 'Updated Label' }); - - // Then - expect(result.updatedAt).toBeDefined(); - expect(result.updatedAt! >= beforeUpdate).toBe(true); - }); - - it('given existing geography then update persists to localStorage', async () => { - // Given - await store.create(mockPopulation1); - - // When - await store.update('user-123', 'geo-456', { label: 'Updated Label' }); - - // Then - const persisted = await store.findById('user-123', 'geo-456'); - expect(persisted?.label).toBe('Updated Label'); - }); - - it('given multiple geographies then updates correct one by composite key', async () => { - // Given - await store.create(mockPopulation1); - await store.create(mockPopulation2); - - // When - await store.update('user-123', 'geo-456', { label: 'Updated Label' }); - - // Then - const updated = await store.findById('user-123', 'geo-456'); - const unchanged = await store.findById('user-123', 'geo-789'); - expect(updated?.label).toBe('Updated Label'); - expect(unchanged?.label).toBe('Test Geography 2'); - }); - - it('given update with partial data then only specified fields are updated', async () => { - // Given - await store.create(mockPopulation1); - - // When - const result = await store.update('user-123', 'geo-456', { label: 'Updated Label' }); - - // Then - expect(result.label).toBe('Updated Label'); - expect(result.scope).toBe('subnational'); // unchanged - }); - }); - - describe('utility methods', () => { - it('given getAllPopulations then returns all stored populations', () => { - // Given - mockLocalStorage['user-geographic-associations'] = JSON.stringify([ - mockPopulation1, - mockPopulation2, - ]); - - // When - const result = store.getAllPopulations(); - - // Then - expect(result).toHaveLength(2); - }); - - it('given clearAllPopulations then removes all data', () => { - // Given - mockLocalStorage['user-geographic-associations'] = JSON.stringify([mockPopulation1]); - - // When - store.clearAllPopulations(); - - // Then - expect(localStorage.removeItem).toHaveBeenCalledWith('user-geographic-associations'); - }); - }); - - describe('error handling', () => { - it('given localStorage error on get then returns empty array', () => { - // Given - (localStorage.getItem as any).mockImplementation(() => { - throw new Error('Storage error'); - }); - - // When - const result = store.getAllPopulations(); - - // Then - expect(result).toEqual([]); - }); - - it('given localStorage error on set then throws error', async () => { - // Given - (localStorage.setItem as any).mockImplementation(() => { - throw new Error('Storage full'); - }); - - // When/Then - await expect(store.create(mockPopulation1)).rejects.toThrow( - 'Failed to store geographic populations in local storage' - ); - }); - }); -}); diff --git a/app/src/tests/unit/hooks/useSharedReportData.test.tsx b/app/src/tests/unit/hooks/useSharedReportData.test.tsx index 83c4e7729..e533f951b 100644 --- a/app/src/tests/unit/hooks/useSharedReportData.test.tsx +++ b/app/src/tests/unit/hooks/useSharedReportData.test.tsx @@ -138,7 +138,7 @@ describe('useSharedReportData', () => { }); }); - test('given valid shareData with geographyId then builds geography object', async () => { + test('given valid shareData with geographyId then builds geography object from simulation data', async () => { // Given vi.mocked(fetchReportById).mockResolvedValue(MOCK_REPORT_METADATA); vi.mocked(fetchSimulationById).mockResolvedValue(MOCK_SIMULATION_METADATA); @@ -152,20 +152,14 @@ describe('useSharedReportData', () => { expect(result.current.isLoading).toBe(false); }); + // Geography is constructed from simulation data expect(result.current.geographies).toHaveLength(1); expect(result.current.geographies[0]).toMatchObject({ id: 'us', countryId: 'us', scope: 'national', }); - - // User geography from ShareData - expect(result.current.userGeographies).toHaveLength(1); - expect(result.current.userGeographies[0]).toMatchObject({ - geographyId: 'us', - label: 'United States', - userId: 'shared', - }); + // Note: userGeographies no longer returned - geographies are not user associations }); test('given shareData with householdId then fetches household', async () => { diff --git a/app/src/tests/unit/hooks/useUserGeographic.test.tsx b/app/src/tests/unit/hooks/useUserGeographic.test.tsx deleted file mode 100644 index 6c6ac7231..000000000 --- a/app/src/tests/unit/hooks/useUserGeographic.test.tsx +++ /dev/null @@ -1,399 +0,0 @@ -import React from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook, waitFor } from '@testing-library/react'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { LocalStorageGeographicStore } from '@/api/geographicAssociation'; -import { - useCreateGeographicAssociation, - useGeographicAssociation, - useGeographicAssociationsByUser, - useUserGeographicStore, -} from '@/hooks/useUserGeographic'; -import { - createMockQueryClient, - GEO_CONSTANTS, - mockUserGeographicAssociation, - mockUserGeographicAssociationList, - QUERY_KEY_PATTERNS, - TEST_IDS, - TEST_LABELS, -} from '@/tests/fixtures/hooks/hooksMocks'; - -// Mock useCurrentCountry hook -vi.mock('@/hooks/useCurrentCountry', () => ({ - useCurrentCountry: vi.fn(() => 'us'), -})); - -// Mock the stores first -vi.mock('@/api/geographicAssociation', () => { - const mockStore = { - create: vi.fn(), - findByUser: vi.fn(), - findById: vi.fn(), - }; - return { - ApiGeographicStore: vi.fn(() => mockStore), - LocalStorageGeographicStore: vi.fn(() => mockStore), - }; -}); - -// Mock query config -vi.mock('@/libs/queryConfig', () => ({ - queryConfig: { - api: { - staleTime: 5 * 60 * 1000, - cacheTime: 10 * 60 * 1000, - }, - localStorage: { - staleTime: 0, - cacheTime: 0, - }, - }, -})); - -// Mock query keys -vi.mock('@/libs/queryKeys', () => ({ - geographicAssociationKeys: { - byUser: (userId: string) => ['geographic-associations', 'byUser', userId], - byGeography: (id: string) => ['geographic-associations', 'byGeography', id], - specific: (userId: string, id: string) => ['geographic-associations', 'specific', userId, id], - }, -})); - -describe('useUserGeographic hooks', () => { - let queryClient: QueryClient; - - beforeEach(() => { - vi.clearAllMocks(); - queryClient = createMockQueryClient(); - - // Get the mock store instance - const mockStore = - (LocalStorageGeographicStore as any).mock.results[0]?.value || - (LocalStorageGeographicStore as any)(); - - // Set default mock implementations - mockStore.create.mockImplementation((input: any) => Promise.resolve(input)); - mockStore.findByUser.mockResolvedValue(mockUserGeographicAssociationList); - mockStore.findById.mockResolvedValue(mockUserGeographicAssociation); - }); - - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - - describe('useUserGeographicStore', () => { - test('given user not logged in then returns local storage store', () => { - // When - const { result } = renderHook(() => useUserGeographicStore()); - - // Then - expect(result.current).toBeDefined(); - expect(result.current.create).toBeDefined(); - expect(result.current.findByUser).toBeDefined(); - expect(result.current.findById).toBeDefined(); - }); - - // Note: Cannot test logged-in case as isLoggedIn is hardcoded to false - // This would need to be refactored to accept auth context - }); - - describe('useGeographicAssociationsByUser', () => { - test('given valid user ID when fetching associations then returns list', async () => { - // Given - const userId = TEST_IDS.USER_ID; - - // When - const { result } = renderHook(() => useGeographicAssociationsByUser(userId), { wrapper }); - - // Then - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data).toEqual(mockUserGeographicAssociationList); - const mockStore = (LocalStorageGeographicStore as any)(); - expect(mockStore.findByUser).toHaveBeenCalledWith(userId, 'us'); - }); - - test('given store throws error when fetching then returns error state', async () => { - // Given - const error = new Error('Failed to fetch associations'); - const mockStore = (LocalStorageGeographicStore as any)(); - mockStore.findByUser.mockRejectedValue(error); - - // When - const { result } = renderHook(() => useGeographicAssociationsByUser(TEST_IDS.USER_ID), { - wrapper, - }); - - // Then - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expect(result.current.error).toEqual(error); - }); - - test('given empty user ID when fetching then still attempts fetch', async () => { - // When - const { result } = renderHook(() => useGeographicAssociationsByUser(''), { wrapper }); - - // Then - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - const mockStore = (LocalStorageGeographicStore as any)(); - expect(mockStore.findByUser).toHaveBeenCalledWith('', 'us'); - }); - - test('given user with no associations then returns empty array', async () => { - // Given - const mockStore = (LocalStorageGeographicStore as any)(); - mockStore.findByUser.mockResolvedValue([]); - - // When - const { result } = renderHook(() => useGeographicAssociationsByUser(TEST_IDS.USER_ID), { - wrapper, - }); - - // Then - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data).toEqual([]); - }); - }); - - describe('useGeographicAssociation', () => { - test('given valid IDs when fetching specific association then returns data', async () => { - // Given - const userId = TEST_IDS.USER_ID; - const geographyId = TEST_IDS.GEOGRAPHY_ID; - - // When - const { result } = renderHook(() => useGeographicAssociation(userId, geographyId), { - wrapper, - }); - - // Then - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data).toEqual(mockUserGeographicAssociation); - const mockStore = (LocalStorageGeographicStore as any)(); - expect(mockStore.findById).toHaveBeenCalledWith(userId, geographyId); - }); - - test('given non-existent association when fetching then returns null', async () => { - // Given - const mockStore = (LocalStorageGeographicStore as any)(); - mockStore.findById.mockResolvedValue(null); - - // When - const { result } = renderHook( - () => useGeographicAssociation(TEST_IDS.USER_ID, 'non-existent'), - { wrapper } - ); - - // Then - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data).toBeNull(); - }); - - test('given error in fetching then returns error state', async () => { - // Given - const error = new Error('Network error'); - const mockStore = (LocalStorageGeographicStore as any)(); - mockStore.findById.mockRejectedValue(error); - - // When - const { result } = renderHook( - () => useGeographicAssociation(TEST_IDS.USER_ID, TEST_IDS.GEOGRAPHY_ID), - { wrapper } - ); - - // Then - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expect(result.current.error).toEqual(error); - }); - }); - - describe('useCreateGeographicAssociation', () => { - test('given valid association when created then updates cache correctly', async () => { - // Given - const newAssociation = { - id: TEST_IDS.GEOGRAPHY_ID, - userId: TEST_IDS.USER_ID, - countryId: GEO_CONSTANTS.COUNTRY_US, - scope: GEO_CONSTANTS.TYPE_NATIONAL, - geographyId: GEO_CONSTANTS.COUNTRY_US, - label: TEST_LABELS.GEOGRAPHY, - } as const; - - const { result } = renderHook(() => useCreateGeographicAssociation(), { wrapper }); - - // When - await result.current.mutateAsync(newAssociation); - - // Then - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - // Verify store was called - const mockStore = (LocalStorageGeographicStore as any)(); - expect(mockStore.create).toHaveBeenCalledWith({ ...newAssociation, type: 'geography' }); - - // Verify cache invalidation - expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: QUERY_KEY_PATTERNS.GEO_ASSOCIATION_BY_USER(TEST_IDS.USER_ID), - }); - expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: QUERY_KEY_PATTERNS.GEO_ASSOCIATION_BY_GEOGRAPHY(GEO_CONSTANTS.COUNTRY_US), - }); - - // Verify cache update - expect(queryClient.setQueryData).toHaveBeenCalledWith( - QUERY_KEY_PATTERNS.GEO_ASSOCIATION_SPECIFIC(TEST_IDS.USER_ID, GEO_CONSTANTS.COUNTRY_US), - { ...newAssociation, type: 'geography' } - ); - }); - - test('given subnational association when created then updates cache with full identifier', async () => { - // Given - const subnationalAssociation = { - ...mockUserGeographicAssociation, - scope: GEO_CONSTANTS.TYPE_SUBNATIONAL, - geographyId: GEO_CONSTANTS.REGION_CA, - countryId: GEO_CONSTANTS.COUNTRY_US, - } as const; - - const mockStore = (LocalStorageGeographicStore as any)(); - mockStore.create.mockResolvedValue(subnationalAssociation); - const { result } = renderHook(() => useCreateGeographicAssociation(), { wrapper }); - - // When - await result.current.mutateAsync(subnationalAssociation); - - // Then - expect(queryClient.setQueryData).toHaveBeenCalledWith( - QUERY_KEY_PATTERNS.GEO_ASSOCIATION_SPECIFIC(TEST_IDS.USER_ID, GEO_CONSTANTS.REGION_CA), - subnationalAssociation - ); - }); - - test('given creation fails when creating then returns error', async () => { - // Given - const error = new Error('Creation failed'); - const mockStore = (LocalStorageGeographicStore as any)(); - mockStore.create.mockRejectedValue(error); - const { result } = renderHook(() => useCreateGeographicAssociation(), { wrapper }); - - // When/Then - await expect( - result.current.mutateAsync({ - id: TEST_IDS.GEOGRAPHY_ID, - userId: TEST_IDS.USER_ID, - countryId: GEO_CONSTANTS.COUNTRY_US, - scope: GEO_CONSTANTS.TYPE_NATIONAL, - geographyId: GEO_CONSTANTS.COUNTRY_US, - label: TEST_LABELS.GEOGRAPHY, - }) - ).rejects.toThrow('Creation failed'); - - // Cache should not be updated - expect(queryClient.setQueryData).not.toHaveBeenCalled(); - }); - - test('given multiple associations created then each updates cache independently', async () => { - // Given - const { result } = renderHook(() => useCreateGeographicAssociation(), { wrapper }); - - const association1 = { - id: TEST_IDS.GEOGRAPHY_ID, - userId: TEST_IDS.USER_ID, - countryId: GEO_CONSTANTS.COUNTRY_US, - scope: GEO_CONSTANTS.TYPE_NATIONAL, - geographyId: GEO_CONSTANTS.COUNTRY_US, - label: TEST_LABELS.GEOGRAPHY, - } as const; - - const association2 = { - id: TEST_IDS.GEOGRAPHY_ID_2, - userId: TEST_IDS.USER_ID, - countryId: GEO_CONSTANTS.COUNTRY_UK, - scope: GEO_CONSTANTS.TYPE_NATIONAL, - geographyId: GEO_CONSTANTS.COUNTRY_UK, - label: TEST_LABELS.GEOGRAPHY_2, - } as const; - - // When - const mockStore = (LocalStorageGeographicStore as any)(); - await result.current.mutateAsync(association1); - mockStore.create.mockResolvedValue({ - ...mockUserGeographicAssociation, - ...association2, - }); - await result.current.mutateAsync(association2); - - // Then - expect(mockStore.create).toHaveBeenCalledTimes(2); - expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(4); // 2 calls per creation - }); - }); - - describe('query configuration', () => { - test('given local storage mode then uses local storage config', async () => { - // When - const { result } = renderHook(() => useGeographicAssociationsByUser(TEST_IDS.USER_ID), { - wrapper, - }); - - // Then - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - // Local storage should have no stale time - const queryState = queryClient.getQueryState( - QUERY_KEY_PATTERNS.GEO_ASSOCIATION_BY_USER(TEST_IDS.USER_ID) - ); - expect(queryState).toBeDefined(); - }); - - test('given refetch called then fetches fresh data', async () => { - // Given - const { result } = renderHook(() => useGeographicAssociationsByUser(TEST_IDS.USER_ID), { - wrapper, - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - // Initial data should be the mock list - expect(result.current.data).toEqual(mockUserGeographicAssociationList); - - // When - const mockStore = (LocalStorageGeographicStore as any)(); - mockStore.findByUser.mockResolvedValue([]); - - // Force refetch - const refetchResult = await result.current.refetch(); - - // Then - expect(refetchResult.data).toEqual([]); - expect(mockStore.findByUser).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/app/src/tests/unit/hooks/utils/useFetchReportIngredients.test.ts b/app/src/tests/unit/hooks/utils/useFetchReportIngredients.test.ts index add3b1edc..52a6361c6 100644 --- a/app/src/tests/unit/hooks/utils/useFetchReportIngredients.test.ts +++ b/app/src/tests/unit/hooks/utils/useFetchReportIngredients.test.ts @@ -30,7 +30,7 @@ describe('useFetchReportIngredients', () => { expect(result.userReport.userId).toBe(TEST_USER_IDS.CUSTOM); expect(result.userSimulations[0].userId).toBe(TEST_USER_IDS.CUSTOM); expect(result.userPolicies[0].userId).toBe(TEST_USER_IDS.CUSTOM); - expect(result.userGeographies[0].userId).toBe(TEST_USER_IDS.CUSTOM); + // Note: userGeographies no longer exist - geographies are not user associations }); test('given input without userReport.id then falls back to reportId', () => { @@ -62,7 +62,7 @@ describe('useFetchReportIngredients', () => { expect(result.userSimulations).toEqual([]); expect(result.userPolicies).toEqual([]); expect(result.userHouseholds).toEqual([]); - expect(result.userGeographies).toEqual([]); + // Note: userGeographies no longer exist - geographies are not user associations }); test('given input then preserves all original fields', () => { @@ -99,15 +99,7 @@ describe('useFetchReportIngredients', () => { expect(result.userPolicies[1].userId).toBe(TEST_USER_IDS.SHARED); }); - test('given geography with all fields then preserves scope and type', () => { - // When - const result = expandUserAssociations(SOCIETY_WIDE_INPUT); - - // Then - const geography = result.userGeographies[0]; - expect(geography.type).toBe('geography'); - expect(geography.scope).toBe('national'); - expect(geography.geographyId).toBe(TEST_IDS.GEOGRAPHIES.NATIONAL); - }); + // Note: geography test removed - userGeographies no longer exist + // Geographies are constructed from simulation data, not stored as user associations }); }); diff --git a/app/src/tests/unit/pages/Populations.page.test.tsx b/app/src/tests/unit/pages/Populations.page.test.tsx deleted file mode 100644 index 799e67d93..000000000 --- a/app/src/tests/unit/pages/Populations.page.test.tsx +++ /dev/null @@ -1,456 +0,0 @@ -import { render, screen, userEvent, waitFor } from '@test-utils'; -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; -import { useGeographicAssociationsByUser } from '@/hooks/useUserGeographic'; -import { useUserHouseholds } from '@/hooks/useUserHousehold'; -import PopulationsPage from '@/pages/Populations.page'; -import { - createEmptyDataState, - createErrorState, - createLoadingState, - mockGeographicAssociationsData, - mockUserHouseholdsData, - POPULATION_COLUMNS, - POPULATION_LABELS, - POPULATION_TEST_IDS, - setupMockConsole, -} from '@/tests/fixtures/pages/populationsMocks'; - -// Mock the hooks first -vi.mock('@/hooks/useUserHousehold', () => ({ - useUserHouseholds: vi.fn(), - useUpdateHouseholdAssociation: vi.fn(() => ({ - mutate: vi.fn(), - isPending: false, - })), -})); - -vi.mock('@/hooks/useUserGeographic', () => ({ - useGeographicAssociationsByUser: vi.fn(), - useUpdateGeographicAssociation: vi.fn(() => ({ - mutate: vi.fn(), - isPending: false, - })), -})); - -// Mock useCurrentCountry -vi.mock('@/hooks/useCurrentCountry', () => ({ - useCurrentCountry: () => 'us', -})); - -// Mock useNavigate -const mockNavigate = vi.fn(); -vi.mock('react-router-dom', async () => { - const actual = await vi.importActual('react-router-dom'); - return { - ...actual, - useNavigate: () => mockNavigate, - }; -}); - -// Mock the constants -vi.mock('@/constants', () => ({ - MOCK_USER_ID: 'test-user-123', - BASE_URL: 'https://api.test.com', - CURRENT_YEAR: '2025', -})); - -describe('PopulationsPage', () => { - let consoleMocks: ReturnType; - - beforeEach(() => { - vi.clearAllMocks(); - consoleMocks = setupMockConsole(); - - // Set default mock implementations - (useUserHouseholds as any).mockReturnValue({ - data: mockUserHouseholdsData, - isLoading: false, - isError: false, - error: null, - }); - - (useGeographicAssociationsByUser as any).mockReturnValue({ - data: mockGeographicAssociationsData, - isLoading: false, - isError: false, - error: null, - }); - }); - - afterEach(() => { - consoleMocks.restore(); - }); - - const renderPage = () => { - return render(); - }; - - describe('initial render', () => { - test('given page loads then displays title and subtitle', () => { - // When - renderPage(); - - // Then - expect( - screen.getByRole('heading', { name: 'Your saved households', level: 2 }) - ).toBeInTheDocument(); - expect(screen.getByText(POPULATION_LABELS.PAGE_SUBTITLE)).toBeInTheDocument(); - }); - - test('given page loads then displays build population button', () => { - // When - renderPage(); - - // Then - expect( - screen.getByRole('button', { name: POPULATION_LABELS.BUILD_BUTTON }) - ).toBeInTheDocument(); - }); - - test('given page loads then fetches user data with correct user ID', () => { - // When - renderPage(); - - // Then - expect(useUserHouseholds).toHaveBeenCalledWith(POPULATION_TEST_IDS.USER_ID); - expect(useGeographicAssociationsByUser).toHaveBeenCalledWith(POPULATION_TEST_IDS.USER_ID); - }); - }); - - describe('data display', () => { - test('given household data available then displays household populations', () => { - // When - renderPage(); - - // Then - expect(screen.getByText(POPULATION_LABELS.HOUSEHOLD_1)).toBeInTheDocument(); - expect(screen.getByText(POPULATION_LABELS.HOUSEHOLD_2)).toBeInTheDocument(); - }); - - test('given geographic data available then displays geographic populations', () => { - // When - renderPage(); - - // Then - expect(screen.getByText(POPULATION_LABELS.GEOGRAPHIC_1)).toBeInTheDocument(); - expect(screen.getByText(POPULATION_LABELS.GEOGRAPHIC_2)).toBeInTheDocument(); - }); - - test('given household with people then displays correct person count', () => { - // When - const { container } = renderPage(); - - // Then - check that person counts appear in the page content - const pageContent = container.textContent || ''; - expect(pageContent).toContain('2 person'); - expect(pageContent).toContain('1 person'); - }); - - test('given geographic association then displays scope details', () => { - // When - const { container } = renderPage(); - - // Then - check for geography-related details in page content - const pageContent = container.textContent || ''; - expect(pageContent).toContain('National'); - expect(pageContent).toContain('Subnational'); - }); - - test('given subnational geography then displays region details', () => { - // When - const { container } = renderPage(); - - // Then - check that region details appear in page content - const pageContent = container.textContent || ''; - expect(pageContent).toContain('State'); - }); - - test('given created dates then displays formatted dates', () => { - // When - renderPage(); - - // Then - // Format dates as 'short-month-day-year' format: "Jan 15, 2025" - const date1 = new Date(POPULATION_TEST_IDS.TIMESTAMP_1).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }); - const date2 = new Date(POPULATION_TEST_IDS.TIMESTAMP_2).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }); - - // Use getAllByText since dates might appear multiple times - const date1Elements = screen.getAllByText(date1); - const date2Elements = screen.getAllByText(date2); - - expect(date1Elements.length).toBeGreaterThan(0); - expect(date2Elements.length).toBeGreaterThan(0); - }); - - test('given no data then displays empty state', () => { - // Given - const emptyState = createEmptyDataState(); - (useUserHouseholds as any).mockReturnValue(emptyState.household); - (useGeographicAssociationsByUser as any).mockReturnValue(emptyState.geographic); - - // When - renderPage(); - - // Then - Check that no population items are displayed - expect(screen.queryByText(POPULATION_LABELS.HOUSEHOLD_1)).not.toBeInTheDocument(); - expect(screen.queryByText(POPULATION_LABELS.GEOGRAPHIC_1)).not.toBeInTheDocument(); - }); - }); - - describe('loading states', () => { - test('given household data loading then shows loading state', () => { - // Given - const loadingState = createLoadingState(true, false); - (useUserHouseholds as any).mockReturnValue(loadingState.household); - (useGeographicAssociationsByUser as any).mockReturnValue(loadingState.geographic); - - // When - renderPage(); - - // Then - Look for the Loader component by its role or test the loading state - const loaderElement = document.querySelector('.mantine-Loader-root'); - expect(loaderElement).toBeInTheDocument(); - }); - - test('given geographic data loading then shows loading state', () => { - // Given - const loadingState = createLoadingState(false, true); - (useUserHouseholds as any).mockReturnValue(loadingState.household); - (useGeographicAssociationsByUser as any).mockReturnValue(loadingState.geographic); - - // When - renderPage(); - - // Then - Look for the Loader component - const loaderElement = document.querySelector('.mantine-Loader-root'); - expect(loaderElement).toBeInTheDocument(); - }); - - test('given both loading then shows single loading state', () => { - // Given - const loadingState = createLoadingState(true, true); - (useUserHouseholds as any).mockReturnValue(loadingState.household); - (useGeographicAssociationsByUser as any).mockReturnValue(loadingState.geographic); - - // When - renderPage(); - - // Then - Check for single loader - const loaderElements = document.querySelectorAll('.mantine-Loader-root'); - expect(loaderElements).toHaveLength(1); - }); - }); - - describe('error states', () => { - test('given household fetch error then displays error message', () => { - // Given - const errorState = createErrorState(true, false); - (useUserHouseholds as any).mockReturnValue(errorState.household); - (useGeographicAssociationsByUser as any).mockReturnValue(errorState.geographic); - - // When - renderPage(); - - // Then - Look for error text containing "Error:" - expect(screen.getByText(/Error:/)).toBeInTheDocument(); - }); - - test('given geographic fetch error then displays error message', () => { - // Given - const errorState = createErrorState(false, true); - (useUserHouseholds as any).mockReturnValue(errorState.household); - (useGeographicAssociationsByUser as any).mockReturnValue(errorState.geographic); - - // When - renderPage(); - - // Then - Look for error text containing "Error:" - expect(screen.getByText(/Error:/)).toBeInTheDocument(); - }); - - test('given both fetch errors then displays single error message', () => { - // Given - const errorState = createErrorState(true, true); - (useUserHouseholds as any).mockReturnValue(errorState.household); - (useGeographicAssociationsByUser as any).mockReturnValue(errorState.geographic); - - // When - renderPage(); - - // Then - Check for single error message - const errorElements = screen.getAllByText(/Error:/); - expect(errorElements).toHaveLength(1); - }); - }); - - describe('user interactions', () => { - test('given user clicks build population then dispatches flow action', async () => { - // Given - const user = userEvent.setup(); - renderPage(); - - // When - const buildButton = screen.getByRole('button', { - name: POPULATION_LABELS.BUILD_BUTTON, - }); - await user.click(buildButton); - - // Then - expect(mockNavigate).toHaveBeenCalledWith('/us/households/create'); - }); - - test('given user selects population then updates selection state', async () => { - // Given - const user = userEvent.setup(); - renderPage(); - - // When - Find and click a checkbox (assuming the IngredientReadView renders checkboxes) - const checkboxes = screen.getAllByRole('checkbox'); - if (checkboxes.length > 0) { - await user.click(checkboxes[0]); - - // Then - await waitFor(() => { - expect(checkboxes[0]).toBeChecked(); - }); - } - }); - }); - - describe('data transformation', () => { - test('given household without label then uses default naming', () => { - // Given - const dataWithoutLabel = [ - { - ...mockUserHouseholdsData[0], - association: { - ...mockUserHouseholdsData[0].association, - label: undefined, - }, - }, - ]; - - (useUserHouseholds as any).mockReturnValue({ - data: dataWithoutLabel, - isLoading: false, - isError: false, - error: null, - }); - - // When - renderPage(); - - // Then - expect( - screen.getByText(`Household #${POPULATION_TEST_IDS.HOUSEHOLD_ID_1}`) - ).toBeInTheDocument(); - }); - - test('given household without created date then displays empty date', () => { - // Given - const dataWithoutDate = [ - { - ...mockUserHouseholdsData[0], - association: { - ...mockUserHouseholdsData[0].association, - createdAt: undefined, - }, - }, - ]; - - (useUserHouseholds as any).mockReturnValue({ - data: dataWithoutDate, - isLoading: false, - isError: false, - error: null, - }); - - // When - renderPage(); - - // Then - Check that the household data is displayed (but without checking for specific date text) - expect( - screen.getByText(mockUserHouseholdsData[0].association.label || 'Household') - ).toBeInTheDocument(); - }); - - test('given household with no people then displays zero count', () => { - // Given - const dataWithNoPeople = [ - { - ...mockUserHouseholdsData[0], - household: { - ...mockUserHouseholdsData[0].household, - household_json: { - people: {}, - families: {}, - }, - }, - }, - ]; - - (useUserHouseholds as any).mockReturnValue({ - data: dataWithNoPeople, - isLoading: false, - isError: false, - error: null, - }); - - // When - const { container } = renderPage(); - - // Then - check that zero person count appears in page content - const pageContent = container.textContent || ''; - expect(pageContent).toContain('0 person'); - }); - - test('given mixed data then displays both household and geographic populations', () => { - // When - const { container } = renderPage(); - - // Then - Verify both types are rendered - expect(screen.getByText(POPULATION_LABELS.HOUSEHOLD_1)).toBeInTheDocument(); - expect(screen.getByText(POPULATION_LABELS.GEOGRAPHIC_1)).toBeInTheDocument(); - - // Verify different detail types in page content - const pageContent = container.textContent || ''; - expect(pageContent).toContain('2 person'); - expect(pageContent).toContain('Subnational'); - }); - }); - - describe('column configuration', () => { - test('given page renders then displays correct column headers without connections', () => { - // When - renderPage(); - - // Then - expect(screen.getByText(POPULATION_COLUMNS.NAME)).toBeInTheDocument(); - expect(screen.getByText(POPULATION_COLUMNS.DATE)).toBeInTheDocument(); - expect(screen.getByText(POPULATION_COLUMNS.DETAILS)).toBeInTheDocument(); - // Connections column should not be present - expect(screen.queryByText(POPULATION_COLUMNS.CONNECTIONS)).not.toBeInTheDocument(); - }); - - test('given column configuration then does not include connections column', () => { - // When - renderPage(); - - // Then - // The component should render successfully without connections column - expect( - screen.getByRole('heading', { name: 'Your saved households', level: 2 }) - ).toBeInTheDocument(); - // Verify data is displayed correctly - expect(screen.getByText(POPULATION_LABELS.HOUSEHOLD_1)).toBeInTheDocument(); - }); - }); -}); diff --git a/app/src/tests/unit/pages/ReportOutput.page.test.tsx b/app/src/tests/unit/pages/ReportOutput.page.test.tsx deleted file mode 100644 index f54516019..000000000 --- a/app/src/tests/unit/pages/ReportOutput.page.test.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import { render, screen } from '@test-utils'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { useUserReportById } from '@/hooks/useUserReports'; -import ReportOutputPage from '@/pages/ReportOutput.page'; -import { - MOCK_GEOGRAPHY_UK_CONSTITUENCY, - MOCK_GEOGRAPHY_UK_COUNTRY, - MOCK_GEOGRAPHY_UK_LOCAL_AUTHORITY, - MOCK_GEOGRAPHY_UK_NATIONAL, - MOCK_REPORT_UK_NATIONAL, - MOCK_REPORT_UK_SUBNATIONAL, - MOCK_REPORT_WITH_YEAR, - MOCK_SIMULATION_GEOGRAPHY, - MOCK_SIMULATION_GEOGRAPHY_UK, - MOCK_SOCIETY_WIDE_OUTPUT, - MOCK_USER_REPORT, - MOCK_USER_REPORT_ID, - MOCK_USER_REPORT_UK, -} from '@/tests/fixtures/pages/ReportOutputPageMocks'; - -// Mock dependencies -vi.mock('react-router-dom', async () => { - const actual = await vi.importActual('react-router-dom'); - return { - ...actual, - useNavigate: () => vi.fn(), - useParams: () => ({ - reportId: MOCK_USER_REPORT_ID, - subpage: 'overview', - view: undefined, - }), - useSearchParams: () => [new URLSearchParams(), vi.fn()], - }; -}); - -vi.mock('@/hooks/useCurrentCountry', () => ({ - useCurrentCountry: () => 'us', -})); - -vi.mock('@/hooks/useUserReports', () => ({ - useUserReportById: vi.fn(() => ({ - userReport: MOCK_USER_REPORT, - report: MOCK_REPORT_WITH_YEAR, - simulations: [MOCK_SIMULATION_GEOGRAPHY], - userSimulations: [], - userPolicies: [], - policies: [], - households: [], - userHouseholds: [], - geographies: [], - userGeographies: [], - isLoading: false, - error: null, - })), -})); - -vi.mock('@/hooks/useUserReportAssociations', () => ({ - useUpdateReportAssociation: vi.fn(() => ({ - mutate: vi.fn(), - isPending: false, - })), - useCreateReportAssociation: vi.fn(() => ({ - mutateAsync: vi.fn(), - isPending: false, - })), -})); - -vi.mock('@/hooks/useSharedReportData', () => ({ - useSharedReportData: vi.fn(() => ({ - report: undefined, - simulations: [], - policies: [], - households: [], - geographies: [], - isLoading: false, - error: null, - })), -})); - -// Mock calculation hooks -vi.mock('@/hooks/useCalculationStatus', () => ({ - useCalculationStatus: vi.fn(() => ({ - status: 'complete', - isInitializing: false, - isPending: false, - isComplete: true, - isError: false, - result: MOCK_SOCIETY_WIDE_OUTPUT, - error: null, - })), -})); - -vi.mock('@/hooks/useReportProgressDisplay', () => ({ - useReportProgressDisplay: vi.fn(() => ({ - displayProgress: 100, - hasCalcStatus: true, - message: 'Complete', - })), -})); - -vi.mock('@/hooks/useStartCalculationOnLoad', () => ({ - useStartCalculationOnLoad: vi.fn(), -})); - -vi.mock('@/hooks/useSaveSharedReport', () => ({ - useSaveSharedReport: vi.fn(() => ({ - saveSharedReport: vi.fn(), - saveResult: null, - setSaveResult: vi.fn(), - isPending: false, - })), -})); - -describe('ReportOutputPage', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - test('given report with year then year is passed to layout', () => { - // Given - MOCK_REPORT has year '2024' - render(); - - // Then - Year should be displayed in the layout - expect(screen.getByText(/Year: 2024/)).toBeInTheDocument(); - }); - - test('given report label then label is displayed', () => { - // Given - render(); - - // Then - expect(screen.getByRole('heading', { name: 'Test Report' })).toBeInTheDocument(); - }); - - test('given society-wide report then overview tabs are shown', () => { - // Given - render(); - - // Then - expect(screen.getByText('Overview')).toBeInTheDocument(); - expect(screen.getByText('Comparative analysis')).toBeInTheDocument(); - expect(screen.getByText('Policy')).toBeInTheDocument(); - expect(screen.getByText('Population')).toBeInTheDocument(); - expect(screen.getByText('Dynamics')).toBeInTheDocument(); - }); - - test('given UK national report then constituency and local authority tabs are shown', () => { - // Given - vi.mocked(useUserReportById).mockReturnValue({ - userReport: MOCK_USER_REPORT_UK, - report: MOCK_REPORT_UK_NATIONAL, - simulations: [MOCK_SIMULATION_GEOGRAPHY_UK], - userSimulations: [], - userPolicies: [], - policies: [], - households: [], - userHouseholds: [], - geographies: [MOCK_GEOGRAPHY_UK_NATIONAL], - userGeographies: [], - isLoading: false, - error: null, - }); - - // When - render(); - - // Then - expect(screen.getByText('Constituencies')).toBeInTheDocument(); - expect(screen.getByText('Local authorities')).toBeInTheDocument(); - }); - - test('given UK country-level report (e.g., England) then constituency and local authority tabs are shown', () => { - // Given - vi.mocked(useUserReportById).mockReturnValue({ - userReport: MOCK_USER_REPORT_UK, - report: MOCK_REPORT_UK_SUBNATIONAL, - simulations: [MOCK_SIMULATION_GEOGRAPHY_UK], - userSimulations: [], - userPolicies: [], - policies: [], - households: [], - userHouseholds: [], - geographies: [MOCK_GEOGRAPHY_UK_COUNTRY], - userGeographies: [], - isLoading: false, - error: null, - }); - - // When - render(); - - // Then - Country-level reports should still show the maps - expect(screen.getByText('Constituencies')).toBeInTheDocument(); - expect(screen.getByText('Local authorities')).toBeInTheDocument(); - }); - - test('given UK subnational constituency report then constituency and local authority tabs are hidden', () => { - // Given - vi.mocked(useUserReportById).mockReturnValue({ - userReport: MOCK_USER_REPORT_UK, - report: MOCK_REPORT_UK_SUBNATIONAL, - simulations: [MOCK_SIMULATION_GEOGRAPHY_UK], - userSimulations: [], - userPolicies: [], - policies: [], - households: [], - userHouseholds: [], - geographies: [MOCK_GEOGRAPHY_UK_CONSTITUENCY], - userGeographies: [], - isLoading: false, - error: null, - }); - - // When - render(); - - // Then - Standard tabs should still be visible - expect(screen.getByText('Overview')).toBeInTheDocument(); - expect(screen.getByText('Comparative analysis')).toBeInTheDocument(); - expect(screen.getByText('Policy')).toBeInTheDocument(); - expect(screen.getByText('Population')).toBeInTheDocument(); - expect(screen.getByText('Dynamics')).toBeInTheDocument(); - - // But constituency and local authority tabs should not be shown - expect(screen.queryByText('Constituencies')).not.toBeInTheDocument(); - expect(screen.queryByText('Local authorities')).not.toBeInTheDocument(); - }); - - test('given UK subnational local authority report then constituency and local authority tabs are hidden', () => { - // Given - vi.mocked(useUserReportById).mockReturnValue({ - userReport: MOCK_USER_REPORT_UK, - report: MOCK_REPORT_UK_SUBNATIONAL, - simulations: [MOCK_SIMULATION_GEOGRAPHY_UK], - userSimulations: [], - userPolicies: [], - policies: [], - households: [], - userHouseholds: [], - geographies: [MOCK_GEOGRAPHY_UK_LOCAL_AUTHORITY], - userGeographies: [], - isLoading: false, - error: null, - }); - - // When - render(); - - // Then - Constituency and local authority tabs should not be shown - expect(screen.queryByText('Constituencies')).not.toBeInTheDocument(); - expect(screen.queryByText('Local authorities')).not.toBeInTheDocument(); - }); -}); diff --git a/app/src/tests/unit/pathways/report/ReportPathwayWrapper.test.tsx b/app/src/tests/unit/pathways/report/ReportPathwayWrapper.test.tsx deleted file mode 100644 index a4c1f7343..000000000 --- a/app/src/tests/unit/pathways/report/ReportPathwayWrapper.test.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { render, screen } from '@test-utils'; -import { useParams } from 'react-router-dom'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { useCreateReport } from '@/hooks/useCreateReport'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; -import { useUserHouseholds } from '@/hooks/useUserHousehold'; -import { useUserPolicies } from '@/hooks/useUserPolicy'; -import { useUserSimulations } from '@/hooks/useUserSimulations'; -import ReportPathwayWrapper from '@/pathways/report/ReportPathwayWrapper'; -import { - mockMetadata, - mockNavigate, - mockOnComplete, - mockUseCreateReport, - mockUseParams, - mockUseParamsInvalid, - mockUseParamsMissing, - mockUseUserGeographics, - mockUseUserHouseholds, - mockUseUserPolicies, - mockUseUserSimulations, - resetAllMocks, -} from '@/tests/fixtures/pathways/report/ReportPathwayWrapperMocks'; - -// Mock all dependencies -vi.mock('react-router-dom', async () => { - const actual = await vi.importActual('react-router-dom'); - return { - ...actual, - useNavigate: () => mockNavigate, - useParams: vi.fn(), - }; -}); - -vi.mock('react-redux', async () => { - const actual = await vi.importActual('react-redux'); - return { - ...actual, - useSelector: vi.fn((selector) => { - if (selector.toString().includes('currentLawId')) { - return mockMetadata.currentLawId; - } - return mockMetadata; - }), - }; -}); - -vi.mock('@/hooks/useUserSimulations', () => ({ - useUserSimulations: vi.fn(), -})); - -vi.mock('@/hooks/useUserPolicy', () => ({ - useUserPolicies: vi.fn(), -})); - -vi.mock('@/hooks/useUserHousehold', () => ({ - useUserHouseholds: vi.fn(), -})); - -vi.mock('@/hooks/useUserGeographic', () => ({ - useUserGeographics: vi.fn(), -})); - -vi.mock('@/hooks/useCreateReport', () => ({ - useCreateReport: vi.fn(), -})); - -vi.mock('@/hooks/usePathwayNavigation', () => ({ - usePathwayNavigation: vi.fn(() => ({ - mode: 'LABEL', - navigateToMode: vi.fn(), - goBack: vi.fn(), - getBackMode: vi.fn(), - })), -})); - -describe('ReportPathwayWrapper', () => { - beforeEach(() => { - resetAllMocks(); - vi.clearAllMocks(); - - // Default mock implementations - vi.mocked(useParams).mockReturnValue(mockUseParams); - vi.mocked(useUserSimulations).mockReturnValue(mockUseUserSimulations); - vi.mocked(useUserPolicies).mockReturnValue(mockUseUserPolicies); - vi.mocked(useUserHouseholds).mockReturnValue(mockUseUserHouseholds); - vi.mocked(useUserGeographics).mockReturnValue(mockUseUserGeographics); - vi.mocked(useCreateReport).mockReturnValue(mockUseCreateReport); - }); - - describe('Error handling', () => { - test('given missing countryId param then shows error message', () => { - // Given - vi.mocked(useParams).mockReturnValue(mockUseParamsMissing); - - // When - render(); - - // Then - expect(screen.getByText(/Country ID not found/i)).toBeInTheDocument(); - }); - - test('given invalid countryId then shows error message', () => { - // Given - vi.mocked(useParams).mockReturnValue(mockUseParamsInvalid); - - // When - render(); - - // Then - expect(screen.getByText(/Invalid country ID/i)).toBeInTheDocument(); - }); - }); - - describe('Basic rendering', () => { - test('given valid countryId then renders without error', () => { - // When - const { container } = render(); - - // Then - Should render something (not just error message) - expect(container).toBeInTheDocument(); - expect(screen.queryByText(/Country ID not found/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/Invalid country ID/i)).not.toBeInTheDocument(); - }); - - test('given wrapper renders then initializes with hooks', () => { - // When - render(); - - // Then - Hooks should have been called (useUserPolicies is used in child components, not wrapper) - expect(useUserSimulations).toHaveBeenCalled(); - expect(useUserHouseholds).toHaveBeenCalled(); - expect(useUserGeographics).toHaveBeenCalled(); - expect(useCreateReport).toHaveBeenCalled(); - }); - }); - - describe('Props handling', () => { - test('given onComplete callback then accepts prop', () => { - // When - const { container } = render(); - - // Then - Component renders with callback - expect(container).toBeInTheDocument(); - }); - - test('given no onComplete callback then renders without error', () => { - // When - const { container } = render(); - - // Then - expect(container).toBeInTheDocument(); - }); - }); - - describe('State initialization', () => { - test('given wrapper renders then initializes report state with country', () => { - // When - render(); - - // Then - No errors, component initialized successfully - expect(screen.queryByText(/error/i)).not.toBeInTheDocument(); - }); - }); -}); diff --git a/app/src/tests/unit/pathways/report/views/ReportSetupView.test.tsx b/app/src/tests/unit/pathways/report/views/ReportSetupView.test.tsx deleted file mode 100644 index b5667c30b..000000000 --- a/app/src/tests/unit/pathways/report/views/ReportSetupView.test.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import { render, screen, userEvent } from '@test-utils'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; -import { useUserHouseholds } from '@/hooks/useUserHousehold'; -import ReportSetupView from '@/pathways/report/views/ReportSetupView'; -import { - mockOnBack, - mockOnCancel, - mockOnNavigateToSimulationSelection, - mockOnNext, - mockOnPrefillPopulation2, - mockReportState, - mockReportStateWithBothConfigured, - mockReportStateWithConfiguredBaseline, - mockUseUserGeographicsEmpty, - mockUseUserHouseholdsEmpty, - resetAllMocks, -} from '@/tests/fixtures/pathways/report/views/ReportViewMocks'; - -vi.mock('@/hooks/useUserHousehold', () => ({ - useUserHouseholds: vi.fn(), - isHouseholdMetadataWithAssociation: vi.fn(), -})); - -vi.mock('@/hooks/useUserGeographic', () => ({ - useUserGeographics: vi.fn(), - isGeographicMetadataWithAssociation: vi.fn(), -})); - -describe('ReportSetupView', () => { - beforeEach(() => { - resetAllMocks(); - vi.clearAllMocks(); - vi.mocked(useUserHouseholds).mockReturnValue(mockUseUserHouseholdsEmpty); - vi.mocked(useUserGeographics).mockReturnValue(mockUseUserGeographicsEmpty); - }); - - describe('Basic rendering', () => { - test('given component renders then displays title', () => { - // When - render( - - ); - - // Then - expect(screen.getByRole('heading', { name: /configure report/i })).toBeInTheDocument(); - }); - - test('given component renders then displays baseline simulation card', () => { - // When - render( - - ); - - // Then - Multiple "Baseline simulation" texts exist, just verify at least one - expect(screen.getAllByText(/baseline simulation/i).length).toBeGreaterThan(0); - }); - - test('given component renders then displays comparison simulation card', () => { - // When - render( - - ); - - // Then - expect(screen.getByText(/comparison simulation/i)).toBeInTheDocument(); - }); - }); - - describe('Unconfigured simulations', () => { - test('given no simulations configured then comparison card shows waiting message', () => { - // When - render( - - ); - - // Then - expect(screen.getByText(/waiting for baseline/i)).toBeInTheDocument(); - }); - - test('given no simulations configured then comparison card is disabled', () => { - // When - const { container } = render( - - ); - - // Then - Find card by looking for the disabled state in the Card component - const cards = container.querySelectorAll('[data-variant^="setupCondition"]'); - const comparisonCard = Array.from(cards).find((card) => - card.textContent?.includes('Comparison simulation') - ); - // The card should have disabled styling or be marked as disabled - expect(comparisonCard).toBeDefined(); - expect(comparisonCard?.textContent).toContain('Waiting for baseline'); - }); - - test('given no simulations configured then primary button is disabled', () => { - // When - render( - - ); - - // Then - const buttons = screen.getAllByRole('button'); - const primaryButton = buttons.find( - (btn) => - btn.textContent?.includes('Configure baseline simulation') && - btn.className?.includes('Button') - ); - expect(primaryButton).toBeDisabled(); - }); - }); - - describe('Baseline configured', () => { - test('given baseline configured with household then comparison is optional', () => { - // When - render( - - ); - - // Then - expect(screen.getByText(/comparison simulation \(optional\)/i)).toBeInTheDocument(); - }); - - test('given baseline configured then comparison card is enabled', () => { - // When - render( - - ); - - // Then - const cards = screen.getAllByRole('button'); - const comparisonCard = cards.find((card) => - card.textContent?.includes('Comparison simulation') - ); - expect(comparisonCard).not.toHaveAttribute('data-disabled', 'true'); - }); - - test('given baseline configured with household then can proceed without comparison', () => { - // When - render( - - ); - - // Then - const buttons = screen.getAllByRole('button'); - const reviewButton = buttons.find((btn) => btn.textContent?.includes('Review report')); - expect(reviewButton).not.toBeDisabled(); - }); - }); - - describe('Both simulations configured', () => { - test('given both simulations configured then shows Review report button', () => { - // When - render( - - ); - - // Then - expect(screen.getByRole('button', { name: /review report/i })).toBeInTheDocument(); - }); - - test('given both simulations configured then Review button is enabled', () => { - // When - render( - - ); - - // Then - expect(screen.getByRole('button', { name: /review report/i })).not.toBeDisabled(); - }); - }); - - describe('User interactions', () => { - test('given user selects baseline card then calls navigation with index 0', async () => { - // Given - const user = userEvent.setup(); - render( - - ); - const cards = screen.getAllByRole('button'); - const baselineCard = cards.find((card) => card.textContent?.includes('Baseline simulation')); - - // When - await user.click(baselineCard!); - const configureButton = screen.getByRole('button', { - name: /configure baseline simulation/i, - }); - await user.click(configureButton); - - // Then - expect(mockOnNavigateToSimulationSelection).toHaveBeenCalledWith(0); - }); - - test('given user selects comparison card when baseline configured then prefills population', async () => { - // Given - const user = userEvent.setup(); - render( - - ); - const cards = screen.getAllByRole('button'); - const comparisonCard = cards.find((card) => - card.textContent?.includes('Comparison simulation') - ); - - // When - await user.click(comparisonCard!); - const configureButton = screen.getByRole('button', { - name: /configure comparison simulation/i, - }); - await user.click(configureButton); - - // Then - expect(mockOnPrefillPopulation2).toHaveBeenCalled(); - expect(mockOnNavigateToSimulationSelection).toHaveBeenCalledWith(1); - }); - - test('given both configured and review clicked then calls onNext', async () => { - // Given - const user = userEvent.setup(); - render( - - ); - - // When - await user.click(screen.getByRole('button', { name: /review report/i })); - - // Then - expect(mockOnNext).toHaveBeenCalled(); - }); - }); - - describe('Navigation actions', () => { - test('given onBack provided then renders back button', () => { - // When - render( - - ); - - // Then - expect(screen.getByRole('button', { name: /back/i })).toBeInTheDocument(); - }); - - test('given onCancel provided then renders cancel button', () => { - // When - render( - - ); - - // Then - expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); - }); - }); -}); diff --git a/app/src/tests/unit/pathways/simulation/SimulationPathwayWrapper.test.tsx b/app/src/tests/unit/pathways/simulation/SimulationPathwayWrapper.test.tsx deleted file mode 100644 index bc88d64ca..000000000 --- a/app/src/tests/unit/pathways/simulation/SimulationPathwayWrapper.test.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { render, screen } from '@test-utils'; -import { useParams } from 'react-router-dom'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { useCreateSimulation } from '@/hooks/useCreateSimulation'; -import { useCurrentCountry } from '@/hooks/useCurrentCountry'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; -import { useUserHouseholds } from '@/hooks/useUserHousehold'; -import { useUserPolicies } from '@/hooks/useUserPolicy'; -import SimulationPathwayWrapper from '@/pathways/simulation/SimulationPathwayWrapper'; -import { - mockMetadata, - mockNavigate, - mockOnComplete, - mockUseCreateSimulation, - mockUseParams, - mockUseUserGeographics, - mockUseUserHouseholds, - mockUseUserPolicies, - resetAllMocks, - TEST_COUNTRY_ID, -} from '@/tests/fixtures/pathways/simulation/SimulationPathwayWrapperMocks'; - -// Mock dependencies -vi.mock('react-router-dom', async () => { - const actual = await vi.importActual('react-router-dom'); - return { - ...actual, - useNavigate: () => mockNavigate, - useParams: vi.fn(), - }; -}); - -vi.mock('react-redux', async () => { - const actual = await vi.importActual('react-redux'); - return { - ...actual, - useSelector: vi.fn((selector) => { - if (selector.toString().includes('currentLawId')) { - return mockMetadata.currentLawId; - } - return mockMetadata; - }), - }; -}); - -vi.mock('@/hooks/useCreateSimulation', () => ({ - useCreateSimulation: vi.fn(), -})); - -vi.mock('@/hooks/useUserPolicy', () => ({ - useUserPolicies: vi.fn(), -})); - -vi.mock('@/hooks/useUserHousehold', () => ({ - useUserHouseholds: vi.fn(), -})); - -vi.mock('@/hooks/useUserGeographic', () => ({ - useUserGeographics: vi.fn(), -})); - -vi.mock('@/hooks/usePathwayNavigation', () => ({ - usePathwayNavigation: vi.fn(() => ({ - mode: 'LABEL', - navigateToMode: vi.fn(), - goBack: vi.fn(), - getBackMode: vi.fn(), - })), -})); - -vi.mock('@/hooks/useCurrentCountry', () => ({ - useCurrentCountry: vi.fn(), -})); - -describe('SimulationPathwayWrapper', () => { - beforeEach(() => { - resetAllMocks(); - vi.clearAllMocks(); - - vi.mocked(useParams).mockReturnValue(mockUseParams); - vi.mocked(useCurrentCountry).mockReturnValue(TEST_COUNTRY_ID); - vi.mocked(useCreateSimulation).mockReturnValue(mockUseCreateSimulation); - vi.mocked(useUserPolicies).mockReturnValue(mockUseUserPolicies); - vi.mocked(useUserHouseholds).mockReturnValue(mockUseUserHouseholds); - vi.mocked(useUserGeographics).mockReturnValue(mockUseUserGeographics); - }); - - describe('Error handling', () => { - test('given missing countryId param then shows error message', () => { - // Given - vi.mocked(useParams).mockReturnValue({}); - vi.mocked(useCurrentCountry).mockImplementation(() => { - throw new Error( - 'useCurrentCountry must be used within country routes (protected by CountryGuard). Got countryId: undefined' - ); - }); - - // When/Then - Should throw error since CountryGuard would prevent this in real app - expect(() => render()).toThrow( - 'useCurrentCountry must be used within country routes' - ); - }); - }); - - describe('Basic rendering', () => { - test('given valid countryId then renders without error', () => { - // When - const { container } = render(); - - // Then - expect(container).toBeInTheDocument(); - expect(screen.queryByText(/Country ID not found/i)).not.toBeInTheDocument(); - }); - - test('given wrapper renders then initializes with hooks', () => { - // Given - Clear previous calls before this specific test - vi.clearAllMocks(); - vi.mocked(useParams).mockReturnValue(mockUseParams); - vi.mocked(useCurrentCountry).mockReturnValue(TEST_COUNTRY_ID); - vi.mocked(useUserPolicies).mockReturnValue(mockUseUserPolicies); - vi.mocked(useUserHouseholds).mockReturnValue(mockUseUserHouseholds); - vi.mocked(useUserGeographics).mockReturnValue(mockUseUserGeographics); - - // When - render(); - - // Then - expect(useUserPolicies).toHaveBeenCalled(); - expect(useUserHouseholds).toHaveBeenCalled(); - expect(useUserGeographics).toHaveBeenCalled(); - }); - }); - - describe('Props handling', () => { - test('given onComplete callback then accepts prop', () => { - // When - const { container } = render(); - - // Then - expect(container).toBeInTheDocument(); - }); - }); -}); diff --git a/app/src/tests/unit/utils/PopulationOps.test.ts b/app/src/tests/unit/utils/PopulationOps.test.ts deleted file mode 100644 index 23bf3cb11..000000000 --- a/app/src/tests/unit/utils/PopulationOps.test.ts +++ /dev/null @@ -1,506 +0,0 @@ -import { beforeEach, describe, expect, test } from 'vitest'; -import { - API_PAYLOAD_KEYS, - createGeographyPopRef, - createHouseholdPopRef, - createUserGeographyPop, - EXPECTED_LABELS, - expectedGeographyAPIPayload, - expectedGeographyCacheKey, - expectedGeographyLabel, - expectedHouseholdAPIPayload, - expectedHouseholdCacheKey, - expectedHouseholdLabel, - expectedUserGeographyLabel, - expectedUserGeographyNationalLabel, - expectedUserHouseholdDefaultLabel, - expectedUserHouseholdLabel, - mockGeographyPopRef1, - mockGeographyPopRef2, - mockGeographyPopRefEmpty, - mockHandlers, - mockHouseholdPopRef1, - mockHouseholdPopRef2, - mockHouseholdPopRefEmpty, - mockUserGeographyPop, - mockUserGeographyPopInvalid, - mockUserGeographyPopNational, - mockUserHouseholdPop, - mockUserHouseholdPopInvalid, - mockUserHouseholdPopNoLabel, - mockUserHouseholdPopNoUser, - POPULATION_COUNTRIES, - POPULATION_IDS, - POPULATION_SCOPES, - resetMockHandlers, - setupMockHandlerReturns, - verifyAPIPayload, -} from '@/tests/fixtures/utils/populationOpsMocks'; -import { - matchPopulation, - matchUserPopulation, - PopulationOps, - UserPopulationOps, -} from '@/utils/PopulationOps'; - -describe('PopulationOps', () => { - describe('matchPopulation', () => { - beforeEach(() => { - resetMockHandlers(); - }); - - test('given household population when matching then calls household handler', () => { - // Given - setupMockHandlerReturns('household result', 'geography result'); - - // When - const result = matchPopulation(mockHouseholdPopRef1, mockHandlers); - - // Then - expect(mockHandlers.household).toHaveBeenCalledWith(mockHouseholdPopRef1); - expect(mockHandlers.household).toHaveBeenCalledTimes(1); - expect(mockHandlers.geography).not.toHaveBeenCalled(); - expect(result).toBe('household result'); - }); - - test('given geography population when matching then calls geography handler', () => { - // Given - setupMockHandlerReturns('household result', 'geography result'); - - // When - const result = matchPopulation(mockGeographyPopRef1, mockHandlers); - - // Then - expect(mockHandlers.geography).toHaveBeenCalledWith(mockGeographyPopRef1); - expect(mockHandlers.geography).toHaveBeenCalledTimes(1); - expect(mockHandlers.household).not.toHaveBeenCalled(); - expect(result).toBe('geography result'); - }); - }); - - describe('matchUserPopulation', () => { - beforeEach(() => { - resetMockHandlers(); - }); - - test('given household user population when matching then calls household handler', () => { - // Given - setupMockHandlerReturns('household user result', 'geography user result'); - - // When - const result = matchUserPopulation(mockUserHouseholdPop, mockHandlers); - - // Then - expect(mockHandlers.household).toHaveBeenCalledWith(mockUserHouseholdPop); - expect(mockHandlers.household).toHaveBeenCalledTimes(1); - expect(mockHandlers.geography).not.toHaveBeenCalled(); - expect(result).toBe('household user result'); - }); - - test('given geography user population when matching then calls geography handler', () => { - // Given - setupMockHandlerReturns('household user result', 'geography user result'); - - // When - const result = matchUserPopulation(mockUserGeographyPop, mockHandlers); - - // Then - expect(mockHandlers.geography).toHaveBeenCalledWith(mockUserGeographyPop); - expect(mockHandlers.geography).toHaveBeenCalledTimes(1); - expect(mockHandlers.household).not.toHaveBeenCalled(); - expect(result).toBe('geography user result'); - }); - }); - - describe('PopulationOps.getId', () => { - test('given household population when getting ID then returns household ID', () => { - // When - const result = PopulationOps.getId(mockHouseholdPopRef1); - - // Then - expect(result).toBe(POPULATION_IDS.HOUSEHOLD_1); - }); - - test('given geography population when getting ID then returns geography ID', () => { - // When - const result = PopulationOps.getId(mockGeographyPopRef1); - - // Then - expect(result).toBe(POPULATION_IDS.GEOGRAPHY_1); - }); - - test('given empty household ID when getting ID then returns empty string', () => { - // When - const result = PopulationOps.getId(mockHouseholdPopRefEmpty); - - // Then - expect(result).toBe(POPULATION_IDS.HOUSEHOLD_EMPTY); - }); - }); - - describe('PopulationOps.getLabel', () => { - test('given household population when getting label then returns formatted label', () => { - // When - const result = PopulationOps.getLabel(mockHouseholdPopRef1); - - // Then - expect(result).toBe(expectedHouseholdLabel); - }); - - test('given geography population when getting label then returns formatted label', () => { - // When - const result = PopulationOps.getLabel(mockGeographyPopRef1); - - // Then - expect(result).toBe(expectedGeographyLabel); - }); - }); - - describe('PopulationOps.getTypeLabel', () => { - test('given household population when getting type label then returns Household', () => { - // When - const result = PopulationOps.getTypeLabel(mockHouseholdPopRef1); - - // Then - expect(result).toBe(EXPECTED_LABELS.HOUSEHOLD_TYPE); - }); - - test('given geography population when getting type label then returns Geography', () => { - // When - const result = PopulationOps.getTypeLabel(mockGeographyPopRef1); - - // Then - expect(result).toBe(EXPECTED_LABELS.GEOGRAPHY_TYPE); - }); - }); - - describe('PopulationOps.toAPIPayload', () => { - test('given household population when converting to API payload then returns correct format', () => { - // When - const result = PopulationOps.toAPIPayload(mockHouseholdPopRef1); - - // Then - expect(result).toEqual(expectedHouseholdAPIPayload); - verifyAPIPayload( - result, - [API_PAYLOAD_KEYS.POPULATION_ID, API_PAYLOAD_KEYS.HOUSEHOLD_ID], - expectedHouseholdAPIPayload - ); - }); - - test('given geography population when converting to API payload then returns correct format', () => { - // When - const result = PopulationOps.toAPIPayload(mockGeographyPopRef1); - - // Then - expect(result).toEqual(expectedGeographyAPIPayload); - verifyAPIPayload( - result, - [API_PAYLOAD_KEYS.GEOGRAPHY_ID, API_PAYLOAD_KEYS.REGION], - expectedGeographyAPIPayload - ); - }); - }); - - describe('PopulationOps.getCacheKey', () => { - test('given household population when getting cache key then returns prefixed key', () => { - // When - const result = PopulationOps.getCacheKey(mockHouseholdPopRef1); - - // Then - expect(result).toBe(expectedHouseholdCacheKey); - }); - - test('given geography population when getting cache key then returns prefixed key', () => { - // When - const result = PopulationOps.getCacheKey(mockGeographyPopRef1); - - // Then - expect(result).toBe(expectedGeographyCacheKey); - }); - }); - - describe('PopulationOps.isValid', () => { - test('given household with valid ID when checking validity then returns true', () => { - // When - const result = PopulationOps.isValid(mockHouseholdPopRef1); - - // Then - expect(result).toBe(true); - }); - - test('given household with empty ID when checking validity then returns false', () => { - // When - const result = PopulationOps.isValid(mockHouseholdPopRefEmpty); - - // Then - expect(result).toBe(false); - }); - - test('given geography with valid ID when checking validity then returns true', () => { - // When - const result = PopulationOps.isValid(mockGeographyPopRef1); - - // Then - expect(result).toBe(true); - }); - - test('given geography with empty ID when checking validity then returns false', () => { - // When - const result = PopulationOps.isValid(mockGeographyPopRefEmpty); - - // Then - expect(result).toBe(false); - }); - }); - - describe('PopulationOps.fromUserPopulation', () => { - test('given household user population when converting then returns household ref', () => { - // When - const result = PopulationOps.fromUserPopulation(mockUserHouseholdPop); - - // Then - expect(result.type).toBe('household'); - expect((result as any).householdId).toBe(POPULATION_IDS.HOUSEHOLD_1); - }); - - test('given geography user population when converting then returns geography ref', () => { - // When - const result = PopulationOps.fromUserPopulation(mockUserGeographyPop); - - // Then - expect(result.type).toBe('geography'); - expect((result as any).geographyId).toBe(POPULATION_IDS.GEOGRAPHY_1); - }); - }); - - describe('PopulationOps.isEqual', () => { - test('given same household populations when comparing then returns true', () => { - // Given - const pop1 = createHouseholdPopRef(POPULATION_IDS.HOUSEHOLD_1); - const pop2 = createHouseholdPopRef(POPULATION_IDS.HOUSEHOLD_1); - - // When - const result = PopulationOps.isEqual(pop1, pop2); - - // Then - expect(result).toBe(true); - }); - - test('given different household populations when comparing then returns false', () => { - // When - const result = PopulationOps.isEqual(mockHouseholdPopRef1, mockHouseholdPopRef2); - - // Then - expect(result).toBe(false); - }); - - test('given same geography populations when comparing then returns true', () => { - // Given - const pop1 = createGeographyPopRef(POPULATION_IDS.GEOGRAPHY_1); - const pop2 = createGeographyPopRef(POPULATION_IDS.GEOGRAPHY_1); - - // When - const result = PopulationOps.isEqual(pop1, pop2); - - // Then - expect(result).toBe(true); - }); - - test('given different geography populations when comparing then returns false', () => { - // When - const result = PopulationOps.isEqual(mockGeographyPopRef1, mockGeographyPopRef2); - - // Then - expect(result).toBe(false); - }); - - test('given household and geography populations when comparing then returns false', () => { - // When - const result = PopulationOps.isEqual(mockHouseholdPopRef1, mockGeographyPopRef1); - - // Then - expect(result).toBe(false); - }); - }); - - describe('PopulationOps.household', () => { - test('given household ID when creating household ref then returns correct structure', () => { - // When - const result = PopulationOps.household(POPULATION_IDS.HOUSEHOLD_1); - - // Then - expect(result).toEqual({ - type: 'household', - householdId: POPULATION_IDS.HOUSEHOLD_1, - }); - }); - - test('given empty household ID when creating household ref then still creates ref', () => { - // When - const result = PopulationOps.household(''); - - // Then - expect(result).toEqual({ - type: 'household', - householdId: '', - }); - }); - }); - - describe('PopulationOps.geography', () => { - test('given geography ID when creating geography ref then returns correct structure', () => { - // When - const result = PopulationOps.geography(POPULATION_IDS.GEOGRAPHY_1); - - // Then - expect(result).toEqual({ - type: 'geography', - geographyId: POPULATION_IDS.GEOGRAPHY_1, - }); - }); - - test('given empty geography ID when creating geography ref then still creates ref', () => { - // When - const result = PopulationOps.geography(''); - - // Then - expect(result).toEqual({ - type: 'geography', - geographyId: '', - }); - }); - }); -}); - -describe('UserPopulationOps', () => { - describe('UserPopulationOps.getId', () => { - test('given household user population when getting ID then returns household ID', () => { - // When - const result = UserPopulationOps.getId(mockUserHouseholdPop); - - // Then - expect(result).toBe(POPULATION_IDS.HOUSEHOLD_1); - }); - - test('given geography user population when getting ID then returns geography ID', () => { - // When - const result = UserPopulationOps.getId(mockUserGeographyPop); - - // Then - expect(result).toBe(POPULATION_IDS.GEOGRAPHY_1); - }); - }); - - describe('UserPopulationOps.getLabel', () => { - test('given user population with label when getting label then returns custom label', () => { - // When - const result = UserPopulationOps.getLabel(mockUserHouseholdPop); - - // Then - expect(result).toBe(expectedUserHouseholdLabel); - }); - - test('given household user population without label when getting label then returns default', () => { - // When - const result = UserPopulationOps.getLabel(mockUserHouseholdPopNoLabel); - - // Then - expect(result).toBe(expectedUserHouseholdDefaultLabel); - }); - - test('given geography user population with label when getting label then returns custom label', () => { - // When - const result = UserPopulationOps.getLabel(mockUserGeographyPop); - - // Then - expect(result).toBe(expectedUserGeographyLabel); - }); - - test('given national geography without label when getting label then returns national format', () => { - // When - const result = UserPopulationOps.getLabel(mockUserGeographyPopNational); - - // Then - expect(result).toBe(expectedUserGeographyNationalLabel); - }); - - test('given subnational geography without label when getting label then returns regional format', () => { - // Given - const subNationalPop = createUserGeographyPop( - POPULATION_IDS.GEOGRAPHY_1, - POPULATION_COUNTRIES.US, - POPULATION_SCOPES.SUBNATIONAL as any, - POPULATION_IDS.USER_1 - ); - - // When - const result = UserPopulationOps.getLabel(subNationalPop); - - // Then - expect(result).toBe(`${EXPECTED_LABELS.REGIONAL_PREFIX} ${POPULATION_IDS.GEOGRAPHY_1}`); - }); - }); - - describe('UserPopulationOps.toPopulationRef', () => { - test('given household user population when converting then returns household ref', () => { - // When - const result = UserPopulationOps.toPopulationRef(mockUserHouseholdPop); - - // Then - expect(result.type).toBe('household'); - expect((result as any).householdId).toBe(POPULATION_IDS.HOUSEHOLD_1); - }); - - test('given geography user population when converting then returns geography ref', () => { - // When - const result = UserPopulationOps.toPopulationRef(mockUserGeographyPop); - - // Then - expect(result.type).toBe('geography'); - expect((result as any).geographyId).toBe(POPULATION_IDS.GEOGRAPHY_1); - }); - }); - - describe('UserPopulationOps.isValid', () => { - test('given valid household user population when checking validity then returns true', () => { - // When - const result = UserPopulationOps.isValid(mockUserHouseholdPop); - - // Then - expect(result).toBe(true); - }); - - test('given household with empty ID when checking validity then returns false', () => { - // When - const result = UserPopulationOps.isValid(mockUserHouseholdPopInvalid); - - // Then - expect(result).toBe(false); - }); - - test('given household with no user ID when checking validity then returns false', () => { - // When - const result = UserPopulationOps.isValid(mockUserHouseholdPopNoUser); - - // Then - expect(result).toBe(false); - }); - - test('given valid geography user population when checking validity then returns true', () => { - // When - const result = UserPopulationOps.isValid(mockUserGeographyPop); - - // Then - expect(result).toBe(true); - }); - - test('given geography with empty ID when checking validity then returns false', () => { - // When - const result = UserPopulationOps.isValid(mockUserGeographyPopInvalid); - - // Then - expect(result).toBe(false); - }); - }); -}); diff --git a/app/src/tests/unit/utils/populationMatching.test.ts b/app/src/tests/unit/utils/populationMatching.test.ts deleted file mode 100644 index 8d50d8b1b..000000000 --- a/app/src/tests/unit/utils/populationMatching.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import type { UserGeographicMetadataWithAssociation } from '@/hooks/useUserGeographic'; -import type { UserHouseholdMetadataWithAssociation } from '@/hooks/useUserHousehold'; -import { - createMockSimulation, - mockGeographicData, - mockGeographicDataWithNumericMismatch, - mockHouseholdData, - mockHouseholdDataWithNumericMismatch, - TEST_GEOGRAPHY_IDS, - TEST_HOUSEHOLD_IDS, -} from '@/tests/fixtures/utils/populationMatchingMocks'; -import { findMatchingPopulation } from '@/utils/populationMatching'; - -describe('populationMatching', () => { - describe('findMatchingPopulation', () => { - it('given null simulation then returns null', () => { - // Given - const simulation = null; - - // When - const result = findMatchingPopulation(simulation, mockHouseholdData, mockGeographicData); - - // Then - expect(result).toBeNull(); - }); - - it('given simulation without populationId then returns null', () => { - // Given - const simulation = createMockSimulation(); // No populationId - - // When - const result = findMatchingPopulation(simulation, mockHouseholdData, mockGeographicData); - - // Then - expect(result).toBeNull(); - }); - - it('given household simulation with matching populationId then returns matched household', () => { - // Given - const simulation = createMockSimulation({ - populationType: 'household', - populationId: TEST_HOUSEHOLD_IDS.HOUSEHOLD_123, - }); - - // When - const result = findMatchingPopulation(simulation, mockHouseholdData, mockGeographicData); - - // Then - expect(result).toBeDefined(); - expect(result).not.toBeNull(); - if (result) { - expect('household' in result).toBe(true); - expect((result as UserHouseholdMetadataWithAssociation).household?.id).toBe( - TEST_HOUSEHOLD_IDS.HOUSEHOLD_123 - ); - } - }); - - it('given household simulation with non-matching populationId then returns null', () => { - // Given - const simulation = createMockSimulation({ - populationType: 'household', - populationId: TEST_HOUSEHOLD_IDS.NON_EXISTENT, - }); - - // When - const result = findMatchingPopulation(simulation, mockHouseholdData, mockGeographicData); - - // Then - expect(result).toBeNull(); - }); - - it('given geography simulation with matching populationId then returns matched geography', () => { - // Given - const simulation = createMockSimulation({ - populationType: 'geography', - populationId: TEST_GEOGRAPHY_IDS.CALIFORNIA, - }); - - // When - const result = findMatchingPopulation(simulation, mockHouseholdData, mockGeographicData); - - // Then - expect(result).toBeDefined(); - expect(result).not.toBeNull(); - if (result) { - expect('geography' in result).toBe(true); - expect((result as UserGeographicMetadataWithAssociation).geography?.id).toBe( - TEST_GEOGRAPHY_IDS.CALIFORNIA - ); - } - }); - - it('given geography simulation with non-matching populationId then returns null', () => { - // Given - const simulation = createMockSimulation({ - populationType: 'geography', - populationId: TEST_GEOGRAPHY_IDS.NON_EXISTENT, - }); - - // When - const result = findMatchingPopulation(simulation, mockHouseholdData, mockGeographicData); - - // Then - expect(result).toBeNull(); - }); - - it('given household simulation with undefined household data then returns null', () => { - // Given - const simulation = createMockSimulation({ - populationType: 'household', - populationId: TEST_HOUSEHOLD_IDS.HOUSEHOLD_123, - }); - - // When - const result = findMatchingPopulation(simulation, undefined, mockGeographicData); - - // Then - expect(result).toBeNull(); - }); - - it('given geography simulation with undefined geographic data then returns null', () => { - // Given - const simulation = createMockSimulation({ - populationType: 'geography', - populationId: TEST_GEOGRAPHY_IDS.CALIFORNIA, - }); - - // When - const result = findMatchingPopulation(simulation, mockHouseholdData, undefined); - - // Then - expect(result).toBeNull(); - }); - - it('given simulation with empty household data array then returns null', () => { - // Given - const simulation = createMockSimulation({ - populationType: 'household', - populationId: TEST_HOUSEHOLD_IDS.HOUSEHOLD_123, - }); - - // When - const result = findMatchingPopulation(simulation, [], mockGeographicData); - - // Then - expect(result).toBeNull(); - }); - - it('given household simulation with numeric populationId matching string household id then returns matched household', () => { - // Given - Simulate the type mismatch that occurred in production - // API sometimes returns populationId as number, but household.id is always string - const simulation = createMockSimulation({ - populationType: 'household', - populationId: TEST_HOUSEHOLD_IDS.NUMERIC_VALUE as any, // Force number type to simulate the bug - }); - - // When - const result = findMatchingPopulation( - simulation, - mockHouseholdDataWithNumericMismatch, - mockGeographicData - ); - - // Then - Should match despite type mismatch (number vs string) - expect(result).toBeDefined(); - expect(result).not.toBeNull(); - if (result) { - expect('household' in result).toBe(true); - expect((result as UserHouseholdMetadataWithAssociation).household?.id).toBe( - TEST_HOUSEHOLD_IDS.NUMERIC_STRING_MATCH - ); - } - }); - - it('given geography simulation with numeric populationId matching string geography id then returns matched geography', () => { - // Given - Simulate the type mismatch for geography - const simulation = createMockSimulation({ - populationType: 'geography', - populationId: TEST_GEOGRAPHY_IDS.NUMERIC_VALUE as any, // Force number type - }); - - // When - const result = findMatchingPopulation( - simulation, - mockHouseholdData, - mockGeographicDataWithNumericMismatch - ); - - // Then - Should match despite type mismatch (number vs string) - expect(result).toBeDefined(); - expect(result).not.toBeNull(); - if (result) { - expect('geography' in result).toBe(true); - expect((result as UserGeographicMetadataWithAssociation).geography?.id).toBe( - TEST_GEOGRAPHY_IDS.NUMERIC_STRING_MATCH - ); - } - }); - }); -}); diff --git a/app/src/tests/unit/utils/shareUtils.test.ts b/app/src/tests/unit/utils/shareUtils.test.ts deleted file mode 100644 index 0a0f7f05d..000000000 --- a/app/src/tests/unit/utils/shareUtils.test.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { - createInvalidShareDataBadCountryId, - createInvalidShareDataBadGeographyScope, - createInvalidShareDataMissingUserReport, - createInvalidShareDataNonArraySimulations, - createInvalidShareDataNullSimulationId, - createShareDataWithoutId, - createUserReportWithoutId, - createUserReportWithoutReportId, - MOCK_USER_GEOGRAPHIES, - MOCK_USER_HOUSEHOLDS, - MOCK_USER_POLICIES, - MOCK_USER_REPORT, - MOCK_USER_SIMULATIONS, - TEST_BASE_REPORT_IDS, - TEST_USER_REPORT_IDS, - VALID_HOUSEHOLD_SHARE_DATA, - VALID_SHARE_DATA, -} from '@/tests/fixtures/utils/shareUtilsMocks'; -import { - buildSharePath, - createShareData, - decodeShareData, - encodeShareData, - extractShareDataFromUrl, - getShareDataUserReportId, - isValidShareData, -} from '@/utils/shareUtils'; - -describe('shareUtils', () => { - describe('encodeShareData / decodeShareData', () => { - test('given valid share data then encodes and decodes back to original', () => { - // When - const encoded = encodeShareData(VALID_SHARE_DATA); - const decoded = decodeShareData(encoded); - - // Then - expect(decoded).toEqual(VALID_SHARE_DATA); - }); - - test('given share data with householdId then round-trips correctly', () => { - // When - const encoded = encodeShareData(VALID_HOUSEHOLD_SHARE_DATA); - const decoded = decodeShareData(encoded); - - // Then - expect(decoded).toEqual(VALID_HOUSEHOLD_SHARE_DATA); - }); - - test('given encoded string then produces URL-safe characters', () => { - // When - const encoded = encodeShareData(VALID_SHARE_DATA); - - // Then - should not contain +, /, or = (standard base64 chars that need URL encoding) - expect(encoded).not.toMatch(/[+/=]/); - // Should only contain URL-safe base64 characters - expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/); - }); - - test('given encoded string then can be used in URL without breaking', () => { - // When - const encoded = encodeShareData(VALID_SHARE_DATA); - - // Then - verify round-trip through URL: encode -> put in URL -> extract -> decode - const url = new URL(`https://example.com/report?share=${encoded}`); - const extractedParam = url.searchParams.get('share'); - const decoded = decodeShareData(extractedParam!); - - expect(decoded).toEqual(VALID_SHARE_DATA); - }); - - test('given invalid base64 string then returns null', () => { - // When - const result = decodeShareData('not-valid-base64!!!'); - - // Then - expect(result).toBeNull(); - }); - - test('given valid base64 but invalid JSON structure then returns null', () => { - // Given - encode a non-ShareData object - const invalidShareData = btoa(JSON.stringify({ foo: 'bar' })); - - // When - const result = decodeShareData(invalidShareData); - - // Then - expect(result).toBeNull(); - }); - }); - - describe('isValidShareData', () => { - test('given valid share data then returns true', () => { - // When - const result = isValidShareData(VALID_SHARE_DATA); - - // Then - expect(result).toBe(true); - }); - - test('given null then returns false', () => { - // When - const result = isValidShareData(null); - - // Then - expect(result).toBe(false); - }); - - test('given object missing userReport then returns false', () => { - // Given - const invalid = createInvalidShareDataMissingUserReport(); - - // When - const result = isValidShareData(invalid); - - // Then - expect(result).toBe(false); - }); - - test('given object with non-array userSimulations then returns false', () => { - // Given - const invalid = createInvalidShareDataNonArraySimulations(); - - // When - const result = isValidShareData(invalid); - - // Then - expect(result).toBe(false); - }); - - test('given object with invalid userSimulation objects then returns false', () => { - // Given - simulationId should be string or number, not null - const invalid = createInvalidShareDataNullSimulationId(); - - // When - const result = isValidShareData(invalid); - - // Then - expect(result).toBe(false); - }); - - test('given object with invalid countryId then returns false', () => { - // Given - const invalid = createInvalidShareDataBadCountryId(); - - // When - const result = isValidShareData(invalid); - - // Then - expect(result).toBe(false); - }); - - test('given object with invalid geography scope then returns false', () => { - // Given - const invalid = createInvalidShareDataBadGeographyScope(); - - // When - const result = isValidShareData(invalid); - - // Then - expect(result).toBe(false); - }); - }); - - describe('buildSharePath', () => { - test('given share data then builds correct path with userReportId', () => { - // When - const path = buildSharePath(VALID_SHARE_DATA); - - // Then - should use userReport.id in path - expect(path).toMatch( - new RegExp(`^/us/report-output/${TEST_USER_REPORT_IDS.SOCIETY_WIDE}\\?share=`) - ); - }); - - test('given share data with different country then uses that country in path', () => { - // When - const path = buildSharePath(VALID_HOUSEHOLD_SHARE_DATA); - - // Then - expect(path).toMatch( - new RegExp(`^/uk/report-output/${TEST_USER_REPORT_IDS.HOUSEHOLD}\\?share=`) - ); - }); - - test('given share data then path contains decodable data', () => { - // When - const path = buildSharePath(VALID_SHARE_DATA); - const shareParam = path.split('share=')[1]; - const decoded = decodeShareData(shareParam); - - // Then - expect(decoded).toEqual(VALID_SHARE_DATA); - }); - }); - - describe('extractShareDataFromUrl', () => { - test('given URL with valid share param then extracts share data', () => { - // Given - const encoded = encodeShareData(VALID_SHARE_DATA); - const searchParams = new URLSearchParams(`share=${encoded}`); - - // When - const result = extractShareDataFromUrl(searchParams); - - // Then - expect(result).toEqual(VALID_SHARE_DATA); - }); - - test('given URL without share param then returns null', () => { - // Given - const searchParams = new URLSearchParams('other=value'); - - // When - const result = extractShareDataFromUrl(searchParams); - - // Then - expect(result).toBeNull(); - }); - - test('given URL with invalid share param then returns null', () => { - // Given - const searchParams = new URLSearchParams('share=invalid-data'); - - // When - const result = extractShareDataFromUrl(searchParams); - - // Then - expect(result).toBeNull(); - }); - }); - - describe('createShareData', () => { - test('given user associations then creates share data with userId stripped', () => { - // When - const result = createShareData( - MOCK_USER_REPORT, - MOCK_USER_SIMULATIONS, - MOCK_USER_POLICIES, - MOCK_USER_HOUSEHOLDS, - MOCK_USER_GEOGRAPHIES - ); - - // Then - result should have all fields except userId/timestamps - expect(result).toMatchObject({ - userReport: { - id: TEST_USER_REPORT_IDS.TEST, - reportId: TEST_BASE_REPORT_IDS.TEST, - countryId: 'us', - label: 'My Report', - }, - userSimulations: [{ simulationId: 'sim-1', countryId: 'us', label: 'Sim Label' }], - userPolicies: [{ policyId: 'policy-1', countryId: 'us', label: 'Policy Label' }], - userHouseholds: [], - userGeographies: [ - { - type: 'geography', - geographyId: 'geo-1', - countryId: 'us', - scope: 'national', - label: 'Geography Label', - }, - ], - }); - // Verify userId was stripped - expect(result?.userReport).not.toHaveProperty('userId'); - expect(result?.userSimulations[0]).not.toHaveProperty('userId'); - expect(result?.userGeographies[0]).not.toHaveProperty('userId'); - }); - - test('given user report without id then returns null', () => { - // Given - const userReport = createUserReportWithoutId(); - - // When - const result = createShareData(userReport, [], [], [], []); - - // Then - expect(result).toBeNull(); - }); - - test('given user report without reportId then returns null', () => { - // Given - const userReport = createUserReportWithoutReportId(); - - // When - const result = createShareData(userReport, [], [], [], []); - - // Then - expect(result).toBeNull(); - }); - }); - - describe('getShareDataUserReportId', () => { - test('given share data with id then returns id', () => { - // When - const result = getShareDataUserReportId(VALID_SHARE_DATA); - - // Then - expect(result).toBe(TEST_USER_REPORT_IDS.SOCIETY_WIDE); - }); - - test('given share data without id then falls back to reportId', () => { - // Given - simulate legacy data without id field - const shareData = createShareDataWithoutId(); - - // When - const result = getShareDataUserReportId(shareData); - - // Then - expect(result).toBe(TEST_BASE_REPORT_IDS.SOCIETY_WIDE); - }); - }); -}); diff --git a/app/src/types/ingredients/UserPopulation.ts b/app/src/types/ingredients/UserPopulation.ts index 28f1a4b4d..d4b143878 100644 --- a/app/src/types/ingredients/UserPopulation.ts +++ b/app/src/types/ingredients/UserPopulation.ts @@ -1,8 +1,8 @@ import { countryIds } from '@/libs/countries'; /** - * UserPopulation type using discriminated union for Household and Geography - * This allows users to associate with either a household or geographic area + * UserPopulation type for household associations. + * Geographic areas are selected directly without user-specific associations. */ interface BaseUserPopulation { @@ -20,15 +20,4 @@ export interface UserHouseholdPopulation extends BaseUserPopulation { countryId: (typeof countryIds)[number]; } -export interface UserGeographyPopulation extends BaseUserPopulation { - type: 'geography'; - geographyId: string; // References Geography.geographyId - // For UK: ALWAYS includes prefix ("constituency/Sheffield Central", "country/england") - // For US: New format ALWAYS includes prefix ("state/ca", "congressional_district/CA-01"); - // previously could be just state code ("ca"); this supports both - // National: Just country code ("uk", "us") - countryId: (typeof countryIds)[number]; - scope: 'national' | 'subnational'; -} - -export type UserPopulation = UserHouseholdPopulation | UserGeographyPopulation; +export type UserPopulation = UserHouseholdPopulation; diff --git a/app/src/types/ingredients/index.ts b/app/src/types/ingredients/index.ts index 5d225ef40..a3e0a4eb1 100644 --- a/app/src/types/ingredients/index.ts +++ b/app/src/types/ingredients/index.ts @@ -7,7 +7,7 @@ import { Population } from './Population'; import { Report } from './Report'; import { Simulation } from './Simulation'; import { UserPolicy } from './UserPolicy'; -import { UserGeographyPopulation, UserHouseholdPopulation, UserPopulation } from './UserPopulation'; +import { UserHouseholdPopulation, UserPopulation } from './UserPopulation'; import { UserReport } from './UserReport'; import { UserSimulation } from './UserSimulation'; @@ -58,10 +58,10 @@ export function isUserPolicy(obj: UserIngredient): obj is UserPolicy { } /** - * Type guard to check if an object is a UserPopulation + * Type guard to check if an object is a UserPopulation (household only) */ export function isUserPopulation(obj: UserIngredient): obj is UserPopulation { - return 'type' in obj && ('householdId' in obj || 'geographyId' in obj); + return 'type' in obj && 'householdId' in obj; } /** @@ -71,13 +71,6 @@ export function isUserHouseholdPopulation(obj: UserPopulation): obj is UserHouse return obj.type === 'household'; } -/** - * Type guard to check if a UserPopulation is for geography - */ -export function isUserGeographyPopulation(obj: UserPopulation): obj is UserGeographyPopulation { - return obj.type === 'geography'; -} - /** * Type guard to check if an object is a UserSimulation */ @@ -96,7 +89,6 @@ export function isUserReport(obj: UserIngredient): obj is UserReport { export type { Geography, Household, Policy, Population, Report, Simulation }; export type { UserPolicy, - UserGeographyPopulation, UserHouseholdPopulation, UserPopulation, UserReport, diff --git a/app/src/utils/PopulationOps.ts b/app/src/utils/PopulationOps.ts index 6b58f5df8..71d5693d3 100644 --- a/app/src/utils/PopulationOps.ts +++ b/app/src/utils/PopulationOps.ts @@ -33,23 +33,6 @@ export function matchPopulation( return handlers.geography(population as GeographyPopulationRef); } -/** - * Helper function for UserPopulation pattern matching - */ -export function matchUserPopulation( - population: UserPopulation, - handlers: { - household: (p: Extract) => T; - geography: (p: Extract) => T; - } -): T { - if (population.type === 'household') { - return handlers.household(population as Extract); - } - - return handlers.geography(population as Extract); -} - /** * Centralized operations for working with population references * Eliminates the need for if/else checks throughout the codebase @@ -119,13 +102,10 @@ export const PopulationOps = { /** * Create a population reference from a UserPopulation + * Note: UserPopulation is now only UserHouseholdPopulation (geography associations removed) */ fromUserPopulation: (up: UserPopulation): PopulationRef => { - if (up.type === 'household') { - return { type: 'household', householdId: up.householdId }; - } - - return { type: 'geography', geographyId: up.geographyId }; + return { type: 'household', householdId: up.householdId }; }, /** @@ -156,28 +136,19 @@ export const PopulationOps = { }; /** - * Operations specific to UserPopulation + * Operations specific to UserPopulation (which is now just UserHouseholdPopulation) + * Note: Geographic populations are no longer stored as user associations. */ export const UserPopulationOps = { /** * Get the ID for a UserPopulation */ - getId: (p: UserPopulation): string => - matchUserPopulation(p, { - household: (h) => h.householdId, - geography: (g) => g.geographyId, - }), + getId: (p: UserPopulation): string => p.householdId, /** * Get a display label for a UserPopulation */ - getLabel: (p: UserPopulation): string => - p.label || - matchUserPopulation(p, { - household: (h) => `Household ${h.householdId}`, - geography: (g) => - `${g.scope === 'national' ? 'National households' : 'Households in'} ${g.geographyId}`, - }), + getLabel: (p: UserPopulation): string => p.label || `Household ${p.householdId}`, /** * Convert UserPopulation to PopulationRef for use in Simulation @@ -187,9 +158,5 @@ export const UserPopulationOps = { /** * Check if a UserPopulation is valid */ - isValid: (p: UserPopulation): boolean => - matchUserPopulation(p, { - household: (h) => !!h.householdId && !!h.userId, - geography: (g) => !!g.geographyId && !!g.countryId, - }), + isValid: (p: UserPopulation): boolean => !!p.householdId && !!p.userId, }; diff --git a/app/src/utils/populationMatching.ts b/app/src/utils/populationMatching.ts index e6c0d757c..306c018da 100644 --- a/app/src/utils/populationMatching.ts +++ b/app/src/utils/populationMatching.ts @@ -1,7 +1,3 @@ -import { - isGeographicMetadataWithAssociation, - UserGeographicMetadataWithAssociation, -} from '@/hooks/useUserGeographic'; import { isHouseholdMetadataWithAssociation, UserHouseholdMetadataWithAssociation, @@ -9,19 +5,20 @@ import { import { Simulation } from '@/types/ingredients/Simulation'; /** - * Finds a matching population from user data based on simulation's populationId. + * Finds a matching household population from user data based on simulation's populationId. * Used in locked mode to auto-populate the population from another simulation. * + * Note: Geographic populations are no longer stored as user associations. + * Geography selection is ephemeral and built from simulation data. + * * @param simulation - The simulation containing the populationId to match * @param householdData - Array of user household populations - * @param geographicData - Array of user geographic populations - * @returns The matched population association, or null if not found + * @returns The matched household population association, or null if not found */ export function findMatchingPopulation( simulation: Simulation | null, - householdData: UserHouseholdMetadataWithAssociation[] | undefined, - geographicData: UserGeographicMetadataWithAssociation[] | undefined -): UserHouseholdMetadataWithAssociation | UserGeographicMetadataWithAssociation | null { + householdData: UserHouseholdMetadataWithAssociation[] | undefined +): UserHouseholdMetadataWithAssociation | null { if (!simulation?.populationId) { return null; } @@ -36,15 +33,6 @@ export function findMatchingPopulation( return match || null; } - // Search in geographic data if it's a geography population - if (simulation.populationType === 'geography' && geographicData) { - const match = geographicData.find( - (g) => - isGeographicMetadataWithAssociation(g) && - String(g.geography?.id) === String(simulation.populationId) - ); - return match || null; - } - + // Geographic populations are constructed from simulation data, not user associations return null; } diff --git a/app/src/utils/shareUtils.ts b/app/src/utils/shareUtils.ts index ba8910837..2d356897b 100644 --- a/app/src/utils/shareUtils.ts +++ b/app/src/utils/shareUtils.ts @@ -14,10 +14,7 @@ import { ReportIngredientsInput } from '@/hooks/utils/useFetchReportIngredients'; import { CountryId, countryIds } from '@/libs/countries'; import { UserPolicy } from '@/types/ingredients/UserPolicy'; -import { - UserGeographyPopulation, - UserHouseholdPopulation, -} from '@/types/ingredients/UserPopulation'; +import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation'; import { UserReport } from '@/types/ingredients/UserReport'; import { UserSimulation } from '@/types/ingredients/UserSimulation'; @@ -157,17 +154,6 @@ export function isValidShareData(data: unknown): data is ReportIngredientsInput return false; } - if ( - !isValidArrayWithStringOrNumberFields( - obj.userGeographies, - ['geographyId', 'countryId'], - (geo) => ['national', 'subnational'].includes(geo.scope as string) - ) - ) { - console.error('[isValidShareData] userGeographies validation failed:', obj.userGeographies); - return false; - } - return true; } @@ -208,8 +194,7 @@ export function createShareData( userReport: UserReport, userSimulations: UserSimulation[], userPolicies: UserPolicy[], - userHouseholds: UserHouseholdPopulation[], - userGeographies: UserGeographyPopulation[] + userHouseholds: UserHouseholdPopulation[] ): ReportIngredientsInput | null { // userReport must have an id and reportId if (!userReport.id || !userReport.reportId) { @@ -229,7 +214,6 @@ export function createShareData( userSimulations: userSimulations.map(stripUserFields), userPolicies: userPolicies.map(stripUserFields), userHouseholds: userHouseholds.map(stripUserFields), - userGeographies: userGeographies.map(stripUserFields), }; } diff --git a/app/src/utils/validation/ingredientValidation.ts b/app/src/utils/validation/ingredientValidation.ts index d845237c2..8164cdd47 100644 --- a/app/src/utils/validation/ingredientValidation.ts +++ b/app/src/utils/validation/ingredientValidation.ts @@ -1,4 +1,3 @@ -import { UserGeographicMetadataWithAssociation } from '@/hooks/useUserGeographic'; import { UserHouseholdMetadataWithAssociation } from '@/hooks/useUserHousehold'; import { PolicyStateProps, PopulationStateProps, SimulationStateProps } from '@/types/pathwayState'; @@ -132,32 +131,3 @@ export function isHouseholdAssociationReady( return true; } -/** - * Checks if a UserGeographicMetadataWithAssociation has fully loaded geography data - * - * A geographic association is considered "ready" when: - * 1. The geography metadata exists (not undefined) - * 2. The query is not still loading - * - * @param association - The geographic association to check - * @returns true if geography data is fully loaded and ready to use - */ -export function isGeographicAssociationReady( - association: UserGeographicMetadataWithAssociation | null | undefined -): boolean { - if (!association) { - return false; - } - - // Still loading individual geography data - if (association.isLoading) { - return false; - } - - // Geography data not loaded - if (!association.geography) { - return false; - } - - return true; -} From f783e108622bb17e56d34e4e36f33c7eec26bc8c Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Fri, 13 Feb 2026 20:11:03 +0100 Subject: [PATCH 04/17] feat: Modify how geography labels work --- .../views/population/PopulationScopeView.tsx | 25 ++++++++++- .../pathwayCallbacks/populationCallbacks.ts | 43 ++++++++++++++----- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/app/src/pathways/report/views/population/PopulationScopeView.tsx b/app/src/pathways/report/views/population/PopulationScopeView.tsx index 265ae9508..cf3c18f1a 100644 --- a/app/src/pathways/report/views/population/PopulationScopeView.tsx +++ b/app/src/pathways/report/views/population/PopulationScopeView.tsx @@ -33,7 +33,7 @@ import USGeographicOptions from '../../components/geographicOptions/USGeographic interface PopulationScopeViewProps { countryId: (typeof countryIds)[number]; regionData: any[]; - onScopeSelected: (geography: Geography | null, scopeType: ScopeType) => void; + onScopeSelected: (geography: Geography | null, scopeType: ScopeType, regionLabel?: string) => void; onBack?: () => void; onCancel?: () => void; } @@ -60,6 +60,24 @@ export default function PopulationScopeView({ setSelectedRegion(''); // Clear selection when scope changes }; + /** + * Get the display label for the selected region + */ + function getRegionLabel(): string { + // National scope uses country name + if (scope === US_REGION_TYPES.NATIONAL) { + return countryId === 'us' ? 'US' : 'UK'; + } + + // For subnational scopes, find the region in the appropriate list + if (!selectedRegion) return ''; + + // Check all region option lists for the selected value + const allOptions = [...usStates, ...usDistricts, ...ukCountries, ...ukConstituencies, ...ukLocalAuthorities]; + const matchedRegion = allOptions.find((r) => r.value === selectedRegion); + return matchedRegion?.label || ''; + } + function submissionHandler() { // Validate that if a regional scope is selected, a region must be chosen const needsRegion = [ @@ -77,7 +95,10 @@ export default function PopulationScopeView({ // Create geography from scope selection const geography = createGeographyFromScope(scope, countryId, selectedRegion); - onScopeSelected(geography as Geography | null, scope); + // Get the region label for display + const regionLabel = getRegionLabel(); + + onScopeSelected(geography as Geography | null, scope, regionLabel); } const formInputs = ( diff --git a/app/src/utils/pathwayCallbacks/populationCallbacks.ts b/app/src/utils/pathwayCallbacks/populationCallbacks.ts index 107bcc154..2bb4c8495 100644 --- a/app/src/utils/pathwayCallbacks/populationCallbacks.ts +++ b/app/src/utils/pathwayCallbacks/populationCallbacks.ts @@ -38,18 +38,41 @@ export function createPopulationCallbacks( ); const handleScopeSelected = useCallback( - (geography: Geography | null, _scopeType: string) => { - setState((prev) => { - const population = populationSelector(prev); - return populationUpdater(prev, { - ...population, - geography: geography || null, - type: geography ? 'geography' : 'household', + (geography: Geography | null, _scopeType: string, regionLabel?: string) => { + // If geography is selected, complete immediately with auto-generated label + if (geography) { + const label = regionLabel ? `${regionLabel} households` : 'Geographic households'; + setState((prev) => { + const population = populationSelector(prev); + return populationUpdater(prev, { + ...population, + geography, + label, + type: 'geography', + }); }); - }); - navigateToMode(labelMode); + + // If custom completion handler is provided, use it (for standalone pathways) + // Otherwise navigate to return mode (for report/simulation pathways) + if (onPopulationComplete?.onGeographyComplete) { + onPopulationComplete.onGeographyComplete(geography.geographyId, label); + } else { + navigateToMode(returnMode); + } + } else { + // Household scope - navigate to label view as before + setState((prev) => { + const population = populationSelector(prev); + return populationUpdater(prev, { + ...population, + geography: null, + type: 'household', + }); + }); + navigateToMode(labelMode); + } }, - [setState, populationSelector, populationUpdater, navigateToMode, labelMode] + [setState, populationSelector, populationUpdater, navigateToMode, labelMode, returnMode, onPopulationComplete] ); const handleSelectExistingHousehold = useCallback( From b32e5d75f0029ea9f86a0b6e840084d0cc04279d Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Fri, 13 Feb 2026 21:04:19 +0100 Subject: [PATCH 05/17] feat: Simplify Geography type --- app/src/hooks/useUserReports.ts | 40 +++-------------- app/src/hooks/useUserSimulations.ts | 17 +++---- .../hooks/utils/useFetchReportIngredients.ts | 27 +++--------- app/src/libs/calculations/CalcOrchestrator.ts | 8 ++-- app/src/pages/Simulations.page.tsx | 2 +- .../pages/report-output/GeographySubPage.tsx | 34 ++++++++------ .../pages/report-output/PopulationSubPage.tsx | 12 ++--- .../report-output/SocietyWideReportOutput.tsx | 4 +- .../pathways/report/views/ReportSetupView.tsx | 8 ++-- .../views/ReportSimulationExistingView.tsx | 7 ++- .../views/ReportSimulationSelectionView.tsx | 21 ++++----- .../report/views/ReportSubmitView.tsx | 6 +-- .../population/GeographicConfirmationView.tsx | 16 ++++--- .../views/population/PopulationLabelView.tsx | 7 +-- .../views/simulation/SimulationSetupView.tsx | 8 ++-- .../views/simulation/SimulationSubmitView.tsx | 11 ++--- .../tests/fixtures/hooks/reportHooksMocks.ts | 8 +--- .../fixtures/hooks/useUserHouseholdMocks.ts | 12 ++--- .../integration/calculationFlowFixtures.ts | 5 +-- .../calculations/orchestrationFixtures.ts | 5 +-- .../libs/calculations/orchestratorFixtures.ts | 4 +- .../libs/calculations/orchestratorMocks.ts | 4 +- .../fixtures/pages/ReportOutputPageMocks.ts | 20 ++------- .../pages/report-output/PopulationSubPage.ts | 14 ++---- .../report-output/SocietyWideReportOutput.ts | 4 +- .../report/views/PopulationViewMocks.ts | 6 +-- .../utils/populationCompatibilityMocks.ts | 10 +---- .../fixtures/utils/populationCopyMocks.ts | 5 +-- .../hooks/useStartCalculationOnLoad.test.tsx | 4 +- .../tests/unit/hooks/useUserReports.test.tsx | 6 +-- .../report-output/GeographySubPage.test.tsx | 12 ++--- .../report-output/PopulationSubPage.test.tsx | 8 ++-- .../tests/unit/utils/geographyUtils.test.ts | 44 +++++-------------- .../utils/populationCompatibility.test.ts | 17 ++----- app/src/types/ingredients/Geography.ts | 24 ++++++---- .../pathwayState/PopulationStateProps.ts | 2 +- app/src/utils/PopulationOps.ts | 17 ++++--- app/src/utils/geographyUtils.ts | 19 ++++---- .../convertSimulationStateToApi.ts | 5 ++- .../reconstructPopulation.ts | 6 +-- .../pathwayCallbacks/populationCallbacks.ts | 15 +++---- app/src/utils/populationCompatibility.ts | 17 ++++--- app/src/utils/regionStrategies.ts | 27 +++--------- .../utils/validation/ingredientValidation.ts | 4 +- 44 files changed, 207 insertions(+), 345 deletions(-) diff --git a/app/src/hooks/useUserReports.ts b/app/src/hooks/useUserReports.ts index f721aa2bc..cb7de1d1a 100644 --- a/app/src/hooks/useUserReports.ts +++ b/app/src/hooks/useUserReports.ts @@ -233,17 +233,11 @@ export const useUserReports = (userId: string) => { reportHouseholds.push(household); } } else if (sim.populationType === 'geography') { - // Create Geography object from the ID - const regionData = geographyOptions?.find((r) => r.name === sim.populationId); - if (regionData) { - reportGeographies.push({ - id: `${sim.countryId}-${sim.populationId}`, - countryId: sim.countryId, - scope: 'subnational' as const, - geographyId: sim.populationId, - name: regionData.label || regionData.name, - } as Geography); - } + // Create Geography object from the regionCode (populationId) + reportGeographies.push({ + countryId: sim.countryId, + regionCode: sim.populationId, + } as Geography); } } }); @@ -456,30 +450,10 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo const geographies: Geography[] = []; simulations.forEach((sim) => { if (sim.populationType === 'geography' && sim.populationId && sim.countryId) { - // Use the simulation's populationId as-is for the Geography id - // The populationId is already in the correct format from createGeographyFromScope - const isNational = sim.populationId === sim.countryId; - - let name: string; - if (isNational) { - name = sim.countryId.toUpperCase(); - } else { - // For subnational, extract the base geography ID and look up in metadata - // e.g., "us-fl" -> "fl", "uk-scotland" -> "scotland" - const parts = sim.populationId.split('-'); - const baseGeographyId = parts.length > 1 ? parts.slice(1).join('-') : sim.populationId; - - // Try to find the label in metadata - const regionData = geographyOptions?.find((r) => r.name === baseGeographyId); - name = regionData?.label || sim.populationId; - } - + // Create simplified Geography with regionCode from simulation's populationId const geography: Geography = { - id: sim.populationId, countryId: sim.countryId, - scope: isNational ? 'national' : 'subnational', - geographyId: sim.populationId, - name, + regionCode: sim.populationId, }; geographies.push(geography); diff --git a/app/src/hooks/useUserSimulations.ts b/app/src/hooks/useUserSimulations.ts index 1ffe8932c..d2f5480b6 100644 --- a/app/src/hooks/useUserSimulations.ts +++ b/app/src/hooks/useUserSimulations.ts @@ -177,18 +177,11 @@ export const useUserSimulations = (userId: string) => { (ha) => ha.householdId === simulation.populationId ); } else if (simulation.populationType === 'geography') { - // Treat as geography - create a Geography object from the ID - const regionData = geographyOptions?.find((r) => r.name === simulation.populationId); - - if (regionData) { - geography = { - id: `${simulation.countryId}-${simulation.populationId}`, - countryId: simulation.countryId, - scope: 'subnational' as const, - geographyId: simulation.populationId, - name: regionData.label || regionData.name, - } as Geography; - } + // Create simplified Geography object from regionCode (populationId) + geography = { + countryId: simulation.countryId, + regionCode: simulation.populationId, + } as Geography; } } diff --git a/app/src/hooks/utils/useFetchReportIngredients.ts b/app/src/hooks/utils/useFetchReportIngredients.ts index 8bb5e6797..d70f8294a 100644 --- a/app/src/hooks/utils/useFetchReportIngredients.ts +++ b/app/src/hooks/utils/useFetchReportIngredients.ts @@ -36,40 +36,25 @@ type GeographyOption = { name: string; label: string }; /** * Construct Geography objects from geography-type simulations * - * Extracts geography metadata from simulations and builds Geography objects. - * For subnational regions, looks up display names from metadata. + * Builds simplified Geography objects using regionCode from simulation's populationId. + * Display names are looked up from region metadata at render time (Phase 6.2). * * @param simulations - Array of simulations to extract geographies from - * @param geographyOptions - Region metadata for name lookups + * @param _geographyOptions - Deprecated: lookup now happens at display time * @returns Array of Geography objects */ export function buildGeographiesFromSimulations( simulations: Simulation[], - geographyOptions: GeographyOption[] | undefined + _geographyOptions: GeographyOption[] | undefined ): Geography[] { const geographies: Geography[] = []; simulations.forEach((sim) => { if (sim.populationType === 'geography' && sim.populationId && sim.countryId) { - const isNational = sim.populationId === sim.countryId; - - let name: string; - if (isNational) { - name = sim.countryId.toUpperCase(); - } else { - // For subnational, extract the base geography ID and look up in metadata - const parts = sim.populationId.split('-'); - const baseGeographyId = parts.length > 1 ? parts.slice(1).join('-') : sim.populationId; - const regionData = geographyOptions?.find((r) => r.name === baseGeographyId); - name = regionData?.label || sim.populationId; - } - + // Create simplified Geography object with regionCode from simulation's populationId geographies.push({ - id: sim.populationId, countryId: sim.countryId, - scope: isNational ? 'national' : 'subnational', - geographyId: sim.populationId, - name, + regionCode: sim.populationId, }); } }); diff --git a/app/src/libs/calculations/CalcOrchestrator.ts b/app/src/libs/calculations/CalcOrchestrator.ts index 5622c4236..1bc86fdcc 100644 --- a/app/src/libs/calculations/CalcOrchestrator.ts +++ b/app/src/libs/calculations/CalcOrchestrator.ts @@ -234,11 +234,11 @@ export class CalcOrchestrator { populationId = config.populations.household1?.id || sim1.populationId || ''; } else { const geography = config.populations.geography1; - // geographyId now contains the FULL prefixed value like "constituency/Sheffield Central" - // For region parameter, prioritize: geography.geographyId → sim1.populationId → countryId + // regionCode contains the FULL prefixed value like "constituency/Sheffield Central" + // For region parameter, prioritize: geography.regionCode → sim1.populationId → countryId // This ensures we use the stored populationId from the simulation if geography is not in config - populationId = geography?.geographyId || sim1.populationId || config.countryId; - region = geography?.geographyId || sim1.populationId || config.countryId; + populationId = geography?.regionCode || sim1.populationId || config.countryId; + region = geography?.regionCode || sim1.populationId || config.countryId; } const calcType = populationType === 'household' ? 'household' : 'societyWide'; diff --git a/app/src/pages/Simulations.page.tsx b/app/src/pages/Simulations.page.tsx index 82abb64a0..182758c36 100644 --- a/app/src/pages/Simulations.page.tsx +++ b/app/src/pages/Simulations.page.tsx @@ -129,7 +129,7 @@ export default function SimulationsPage() { population: { text: item.userHousehold?.label || - item.geography?.name || + item.geography?.regionCode || (item.household ? `Household #${item.household.id}` : 'No population'), } as TextValue, })) || []; diff --git a/app/src/pages/report-output/GeographySubPage.tsx b/app/src/pages/report-output/GeographySubPage.tsx index bba226d74..59b4cafc4 100644 --- a/app/src/pages/report-output/GeographySubPage.tsx +++ b/app/src/pages/report-output/GeographySubPage.tsx @@ -1,22 +1,28 @@ import { Box, Table, Text } from '@mantine/core'; import { colors, spacing, typography } from '@/designTokens'; -import { Geography } from '@/types/ingredients/Geography'; -import { capitalize } from '@/utils/stringUtils'; +import { Geography, isNationalGeography } from '@/types/ingredients/Geography'; interface GeographySubPageProps { baselineGeography?: Geography; reformGeography?: Geography; } +/** + * Get display scope (national/subnational) from geography + */ +function getGeographyScope(geography: Geography | undefined): string { + if (!geography) return '—'; + return isNationalGeography(geography) ? 'National' : 'Subnational'; +} + /** * GeographySubPage - Displays geography population information in Design 4 table format * * Shows baseline and reform geographies side-by-side in a comparison table. * Collapses columns when both simulations use the same geography. * - * Note: Geography names come directly from the Geography objects (constructed from - * simulation data), not from user associations since geographies are no longer - * stored as user associations. + * TODO (Phase 6.2): Look up display labels from region metadata using regionCode + * Currently displays regionCode directly as a fallback. */ export default function GeographySubPage({ baselineGeography, @@ -26,24 +32,24 @@ export default function GeographySubPage({ return
No geography data available
; } - // Check if geographies are the same - const geographiesAreSame = baselineGeography?.id === reformGeography?.id; + // Check if geographies are the same by comparing regionCode + const geographiesAreSame = baselineGeography?.regionCode === reformGeography?.regionCode; - // Get labels from geography names - const baselineLabel = baselineGeography?.name || 'Baseline'; - const reformLabel = reformGeography?.name || 'Reform'; + // Get labels - TODO (Phase 6.2): look up from region metadata + const baselineLabel = baselineGeography?.regionCode || 'Baseline'; + const reformLabel = reformGeography?.regionCode || 'Reform'; // Define table rows const rows = [ { label: 'Geographic area', - baselineValue: baselineGeography?.name || '—', - reformValue: reformGeography?.name || '—', + baselineValue: baselineGeography?.regionCode || '—', + reformValue: reformGeography?.regionCode || '—', }, { label: 'Type', - baselineValue: baselineGeography?.scope ? capitalize(baselineGeography.scope) : '—', - reformValue: reformGeography?.scope ? capitalize(reformGeography.scope) : '—', + baselineValue: getGeographyScope(baselineGeography), + reformValue: getGeographyScope(reformGeography), }, ]; diff --git a/app/src/pages/report-output/PopulationSubPage.tsx b/app/src/pages/report-output/PopulationSubPage.tsx index f26f3371c..dcb8f1f2a 100644 --- a/app/src/pages/report-output/PopulationSubPage.tsx +++ b/app/src/pages/report-output/PopulationSubPage.tsx @@ -55,13 +55,13 @@ export default function PopulationSubPage({ // Handle geography population type // Note: Geographies are constructed from simulation data, not user associations if (populationType === 'geography') { - // Extract geography IDs from simulations - const baselineGeographyId = baselineSimulation?.populationId; - const reformGeographyId = reformSimulation?.populationId; + // Extract regionCodes from simulations (stored in populationId) + const baselineRegionCode = baselineSimulation?.populationId; + const reformRegionCode = reformSimulation?.populationId; - // Find the geographies - match by full id - const baselineGeography = geographies?.find((g) => g.id === baselineGeographyId); - const reformGeography = geographies?.find((g) => g.id === reformGeographyId); + // Find the geographies - match by regionCode + const baselineGeography = geographies?.find((g) => g.regionCode === baselineRegionCode); + const reformGeography = geographies?.find((g) => g.regionCode === reformRegionCode); return ( { @@ -130,7 +129,7 @@ export default function ReportSimulationExistingView({ const policyLabel = enhancedSim.userPolicy?.label || enhancedSim.policy?.label || enhancedSim.policy?.id; const populationLabel = - enhancedSim.userHousehold?.label || enhancedSim.geography?.name || simulation.populationId; + enhancedSim.userHousehold?.label || enhancedSim.geography?.regionCode || simulation.populationId; if (policyLabel && populationLabel) { subtitle = subtitle diff --git a/app/src/pathways/report/views/ReportSimulationSelectionView.tsx b/app/src/pathways/report/views/ReportSimulationSelectionView.tsx index 1466783e3..035e1754f 100644 --- a/app/src/pathways/report/views/ReportSimulationSelectionView.tsx +++ b/app/src/pathways/report/views/ReportSimulationSelectionView.tsx @@ -36,19 +36,16 @@ function createCurrentLawPolicy(currentLawId: number): PolicyStateProps { */ function createNationwidePopulation( countryId: string, - geographyId: string, + regionCode: string, countryName: string ): PopulationStateProps { return { - label: `${countryName} nationwide`, + label: `${countryName} households`, type: 'geography', household: null, geography: { - id: geographyId, countryId: countryId as any, - scope: 'national', - geographyId: countryId, - name: 'National', + regionCode, }, }; } @@ -139,10 +136,10 @@ export default function ReportSimulationSelectionView({ } const countryName = countryNames[countryId] || countryId.toUpperCase(); - const geographyId = existingBaseline.geography?.geographyId || countryId; + const regionCode = existingBaseline.geography?.regionCode || countryId; const policy = createCurrentLawPolicy(currentLawId); - const population = createNationwidePopulation(countryId, geographyId, countryName); + const population = createNationwidePopulation(countryId, regionCode, countryName); const simulationState = createSimulationState( existingSimulationId, simulationLabel, @@ -157,7 +154,7 @@ export default function ReportSimulationSelectionView({ /** * Creates a new default baseline simulation * Note: Geographies are no longer stored as user associations. The geography - * is constructed from simulation data using the countryId as the geographyId. + * is constructed from simulation data using the countryId as the regionCode. */ async function createNewBaseline() { if (!onSelectDefaultBaseline) { @@ -166,12 +163,12 @@ export default function ReportSimulationSelectionView({ setIsCreatingBaseline(true); const countryName = countryNames[countryId] || countryId.toUpperCase(); - const geographyId = countryId; // National geography uses countryId + const regionCode = countryId; // National geography uses countryId as regionCode try { // Create simulation directly - geography is not stored as user association const simulationData: Partial = { - populationId: geographyId, + populationId: regionCode, policyId: currentLawId.toString(), populationType: 'geography', }; @@ -184,7 +181,7 @@ export default function ReportSimulationSelectionView({ const simulationId = data.result.simulation_id; const policy = createCurrentLawPolicy(currentLawId); - const population = createNationwidePopulation(countryId, geographyId, countryName); + const population = createNationwidePopulation(countryId, regionCode, countryName); const simulationState = createSimulationState( simulationId, simulationLabel, diff --git a/app/src/pathways/report/views/ReportSubmitView.tsx b/app/src/pathways/report/views/ReportSubmitView.tsx index 7a188922d..08161d840 100644 --- a/app/src/pathways/report/views/ReportSubmitView.tsx +++ b/app/src/pathways/report/views/ReportSubmitView.tsx @@ -31,7 +31,7 @@ export default function ReportSubmitView({ // Get population label - use label if available, otherwise fall back to ID const populationLabel = simulation.population.label || - `Population #${simulation.population.household?.id || simulation.population.geography?.id}`; + `Population #${simulation.population.household?.id || simulation.population.geography?.regionCode}`; return `${policyLabel} • ${populationLabel}`; }; @@ -40,11 +40,11 @@ export default function ReportSubmitView({ const isSimulation1Configured = !!simulation1?.id || (!!simulation1?.policy?.id && - !!(simulation1?.population?.household?.id || simulation1?.population?.geography?.id)); + !!(simulation1?.population?.household?.id || simulation1?.population?.geography?.regionCode)); const isSimulation2Configured = !!simulation2?.id || (!!simulation2?.policy?.id && - !!(simulation2?.population?.household?.id || simulation2?.population?.geography?.id)); + !!(simulation2?.population?.household?.id || simulation2?.population?.geography?.regionCode)); // Create summary boxes based on the simulations const summaryBoxes: SummaryBoxItem[] = [ diff --git a/app/src/pathways/report/views/population/GeographicConfirmationView.tsx b/app/src/pathways/report/views/population/GeographicConfirmationView.tsx index b60d66057..94505cf31 100644 --- a/app/src/pathways/report/views/population/GeographicConfirmationView.tsx +++ b/app/src/pathways/report/views/population/GeographicConfirmationView.tsx @@ -1,10 +1,14 @@ /** * GeographicConfirmationView - View for confirming geographic population selection * Users can select a geography for simulation without creating a user association + * + * Note: With Phase 2 changes, this view is typically skipped for geography selections. + * It remains for potential future use or edge cases. */ import { Stack, Text } from '@mantine/core'; import PathwayView from '@/components/common/PathwayView'; +import { isNationalGeography } from '@/types/ingredients/Geography'; import { MetadataRegionEntry } from '@/types/metadata'; import { PopulationStateProps } from '@/types/pathwayState'; import { getCountryLabel, getRegionLabel, getRegionTypeLabel } from '@/utils/geographyUtils'; @@ -12,7 +16,7 @@ import { getCountryLabel, getRegionLabel, getRegionTypeLabel } from '@/utils/geo interface GeographicConfirmationViewProps { population: PopulationStateProps; regions: MetadataRegionEntry[]; - onSubmitSuccess: (geographyId: string, label: string) => void; + onSubmitSuccess: (regionCode: string, label: string) => void; onBack?: () => void; } @@ -27,9 +31,9 @@ export default function GeographicConfirmationView({ return; } - const geographyId = population.geography.geographyId; - const label = population.geography.name || getRegionLabel(geographyId, regions); - onSubmitSuccess(geographyId, label); + const regionCode = population.geography.regionCode; + const label = getRegionLabel(regionCode, regions); + onSubmitSuccess(regionCode, label); }; // Build display content based on geographic scope @@ -43,8 +47,9 @@ export default function GeographicConfirmationView({ } const geographyCountryId = population.geography.countryId; + const regionCode = population.geography.regionCode; - if (population.geography.scope === 'national') { + if (isNationalGeography(population.geography)) { return ( @@ -61,7 +66,6 @@ export default function GeographicConfirmationView({ } // Subnational - const regionCode = population.geography.geographyId; const regionLabel = getRegionLabel(regionCode, regions); const regionTypeName = getRegionTypeLabel(geographyCountryId, regionCode, regions); diff --git a/app/src/pathways/report/views/population/PopulationLabelView.tsx b/app/src/pathways/report/views/population/PopulationLabelView.tsx index dfad4fbe1..87bb1bcf4 100644 --- a/app/src/pathways/report/views/population/PopulationLabelView.tsx +++ b/app/src/pathways/report/views/population/PopulationLabelView.tsx @@ -8,6 +8,7 @@ import { useState } from 'react'; import { Stack, Text, TextInput } from '@mantine/core'; import PathwayView from '@/components/common/PathwayView'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { isNationalGeography } from '@/types/ingredients/Geography'; import { PathwayMode } from '@/types/pathwayModes/PathwayMode'; import { PopulationStateProps } from '@/types/pathwayState'; import { extractRegionDisplayValue } from '@/utils/regionStrategies'; @@ -47,11 +48,11 @@ export default function PopulationLabelView({ if (population?.geography) { // Geographic population - if (population.geography.scope === 'national') { + if (isNationalGeography(population.geography)) { return 'National Households'; - } else if (population.geography.geographyId) { + } else if (population.geography.regionCode) { // Use display value (strip prefix for UK regions) - const displayValue = extractRegionDisplayValue(population.geography.geographyId); + const displayValue = extractRegionDisplayValue(population.geography.regionCode); return `${displayValue} Households`; } return 'Regional Households'; diff --git a/app/src/pathways/report/views/simulation/SimulationSetupView.tsx b/app/src/pathways/report/views/simulation/SimulationSetupView.tsx index f760799e9..807950d81 100644 --- a/app/src/pathways/report/views/simulation/SimulationSetupView.tsx +++ b/app/src/pathways/report/views/simulation/SimulationSetupView.tsx @@ -81,7 +81,7 @@ export default function SimulationSetupView({ return `Household #${population.household.id}`; } if (population.geography) { - return `Household(s) #${population.geography.id}`; + return `Household(s) (${population.geography.regionCode})`; } return ''; } @@ -93,16 +93,16 @@ export default function SimulationSetupView({ // In simulation 2 of a report, indicate population is inherited from baseline if (isSimulation2InReport) { - const popId = population.household?.id || population.geography?.id; + const popId = population.household?.id || population.geography?.regionCode; const popType = population.household ? 'Household' : 'Household collection'; - return `${popType} #${popId} • Inherited from baseline simulation`; + return `${popType} ${popId} • Inherited from baseline simulation`; } if (population.label && population.household) { return `Household #${population.household.id}`; } if (population.label && population.geography) { - return `Household collection #${population.geography.id}`; + return `Household collection (${population.geography.regionCode})`; } return ''; } diff --git a/app/src/pathways/report/views/simulation/SimulationSubmitView.tsx b/app/src/pathways/report/views/simulation/SimulationSubmitView.tsx index bdf55181f..f3853a382 100644 --- a/app/src/pathways/report/views/simulation/SimulationSubmitView.tsx +++ b/app/src/pathways/report/views/simulation/SimulationSubmitView.tsx @@ -34,8 +34,8 @@ export default function SimulationSubmitView({ if (simulation.population.household?.id) { populationId = simulation.population.household.id; populationType = 'household'; - } else if (simulation.population.geography?.id) { - populationId = simulation.population.geography.id; + } else if (simulation.population.geography?.regionCode) { + populationId = simulation.population.geography.regionCode; populationType = 'geography'; } @@ -57,16 +57,17 @@ export default function SimulationSubmitView({ } // Create summary boxes based on the current simulation state + const populationIdentifier = simulation.population.household?.id || simulation.population.geography?.regionCode; const summaryBoxes: SummaryBoxItem[] = [ { title: 'Population added', description: simulation.population.label || - `Household #${simulation.population.household?.id || simulation.population.geography?.id}`, - isFulfilled: !!(simulation.population.household?.id || simulation.population.geography?.id), + `Household ${populationIdentifier}`, + isFulfilled: !!populationIdentifier, badge: simulation.population.label || - `Household #${simulation.population.household?.id || simulation.population.geography?.id}`, + `Household ${populationIdentifier}`, }, { title: 'Policy reform added', diff --git a/app/src/tests/fixtures/hooks/reportHooksMocks.ts b/app/src/tests/fixtures/hooks/reportHooksMocks.ts index 1293dffbf..4ddee2a27 100644 --- a/app/src/tests/fixtures/hooks/reportHooksMocks.ts +++ b/app/src/tests/fixtures/hooks/reportHooksMocks.ts @@ -65,18 +65,14 @@ export const mockHousehold: Household = { // Mock Geography - National export const mockNationalGeography: Geography = { - id: 'us', countryId: 'us', - scope: 'national', - geographyId: 'us', + regionCode: 'us', }; // Mock Geography - Subnational export const mockSubnationalGeography: Geography = { - id: 'california', countryId: 'us', - scope: 'subnational', - geographyId: 'california', + regionCode: 'california', }; // Mock Simulations diff --git a/app/src/tests/fixtures/hooks/useUserHouseholdMocks.ts b/app/src/tests/fixtures/hooks/useUserHouseholdMocks.ts index 9cf217142..21b441133 100644 --- a/app/src/tests/fixtures/hooks/useUserHouseholdMocks.ts +++ b/app/src/tests/fixtures/hooks/useUserHouseholdMocks.ts @@ -117,17 +117,13 @@ export const mockHouseholdMetadata2 = { // Mock Geography objects (for use in simulations, not user associations) export const mockGeography1: Geography = { - id: TEST_GEOGRAPHY_ID_1, - countryId: 'us' as any, - scope: 'national', - geographyId: 'us', + countryId: 'us', + regionCode: 'us', }; export const mockGeography2: Geography = { - id: TEST_GEOGRAPHY_ID_2, - countryId: 'us' as any, - scope: 'subnational', - geographyId: 'ca', + countryId: 'us', + regionCode: 'ca', }; // Mock hook return values diff --git a/app/src/tests/fixtures/integration/calculationFlowFixtures.ts b/app/src/tests/fixtures/integration/calculationFlowFixtures.ts index 2a967ed6e..6b1f52093 100644 --- a/app/src/tests/fixtures/integration/calculationFlowFixtures.ts +++ b/app/src/tests/fixtures/integration/calculationFlowFixtures.ts @@ -101,11 +101,8 @@ export const mockSocietyWideCalcConfig = ( household1: null, household2: null, geography1: { - id: 'us-us', countryId: 'us', - scope: 'national', - geographyId: INTEGRATION_TEST_CONSTANTS.GEOGRAPHY_IDS.US_NATIONAL, - name: 'United States', + regionCode: INTEGRATION_TEST_CONSTANTS.GEOGRAPHY_IDS.US_NATIONAL, }, geography2: null, }, diff --git a/app/src/tests/fixtures/libs/calculations/orchestrationFixtures.ts b/app/src/tests/fixtures/libs/calculations/orchestrationFixtures.ts index 8a1605096..4776ddc10 100644 --- a/app/src/tests/fixtures/libs/calculations/orchestrationFixtures.ts +++ b/app/src/tests/fixtures/libs/calculations/orchestrationFixtures.ts @@ -51,11 +51,8 @@ export const mockHousehold = (overrides?: Partial): Household => ({ * Mock Geography */ export const mockGeography = (overrides?: Partial): Geography => ({ - id: 'us-california', countryId: 'us', - scope: 'subnational', - geographyId: ORCHESTRATION_TEST_CONSTANTS.TEST_GEOGRAPHY_ID, - name: 'California', + regionCode: ORCHESTRATION_TEST_CONSTANTS.TEST_GEOGRAPHY_ID, ...overrides, }); diff --git a/app/src/tests/fixtures/libs/calculations/orchestratorFixtures.ts b/app/src/tests/fixtures/libs/calculations/orchestratorFixtures.ts index 5ec52f6ef..ac121ac90 100644 --- a/app/src/tests/fixtures/libs/calculations/orchestratorFixtures.ts +++ b/app/src/tests/fixtures/libs/calculations/orchestratorFixtures.ts @@ -36,10 +36,8 @@ export const createMockCalcConfig = (overrides?: Partial): Calc household1: null, household2: null, geography1: { - id: 'us-us', countryId: ORCHESTRATOR_TEST_CONSTANTS.COUNTRY_IDS.US, - scope: 'national', - geographyId: 'us', + regionCode: 'us', }, geography2: null, }, diff --git a/app/src/tests/fixtures/libs/calculations/orchestratorMocks.ts b/app/src/tests/fixtures/libs/calculations/orchestratorMocks.ts index 37746612b..00f162e57 100644 --- a/app/src/tests/fixtures/libs/calculations/orchestratorMocks.ts +++ b/app/src/tests/fixtures/libs/calculations/orchestratorMocks.ts @@ -119,10 +119,8 @@ export const mockSocietyWideCalcConfig = ( }, populations: { geography1: { - geographyId: TEST_POPULATION_IDS.US, - id: 'us-us', countryId: TEST_COUNTRIES.US, - scope: 'national' as const, + regionCode: TEST_POPULATION_IDS.US, }, }, ...overrides, diff --git a/app/src/tests/fixtures/pages/ReportOutputPageMocks.ts b/app/src/tests/fixtures/pages/ReportOutputPageMocks.ts index f77b49a70..2f2d6ac5e 100644 --- a/app/src/tests/fixtures/pages/ReportOutputPageMocks.ts +++ b/app/src/tests/fixtures/pages/ReportOutputPageMocks.ts @@ -84,35 +84,23 @@ export const MOCK_SIMULATION_GEOGRAPHY_UK = { }; export const MOCK_GEOGRAPHY_UK_NATIONAL: Geography = { - id: 'uk-uk', countryId: 'uk', - scope: 'national', - geographyId: 'uk', - name: 'United Kingdom', + regionCode: 'uk', }; export const MOCK_GEOGRAPHY_UK_COUNTRY: Geography = { - id: 'uk-england', countryId: 'uk', - scope: 'subnational', - geographyId: 'country/england', - name: 'England', + regionCode: 'country/england', }; export const MOCK_GEOGRAPHY_UK_CONSTITUENCY: Geography = { - id: 'uk-sheffield-central', countryId: 'uk', - scope: 'subnational', - geographyId: 'constituency/Sheffield Central', - name: 'Sheffield Central', + regionCode: 'constituency/Sheffield Central', }; export const MOCK_GEOGRAPHY_UK_LOCAL_AUTHORITY: Geography = { - id: 'uk-manchester', countryId: 'uk', - scope: 'subnational', - geographyId: 'local_authority/Manchester', - name: 'Manchester', + regionCode: 'local_authority/Manchester', }; export const MOCK_SOCIETY_WIDE_OUTPUT = { diff --git a/app/src/tests/fixtures/pages/report-output/PopulationSubPage.ts b/app/src/tests/fixtures/pages/report-output/PopulationSubPage.ts index 899105856..12a3260e6 100644 --- a/app/src/tests/fixtures/pages/report-output/PopulationSubPage.ts +++ b/app/src/tests/fixtures/pages/report-output/PopulationSubPage.ts @@ -15,8 +15,8 @@ export const TEST_HOUSEHOLD_IDS = { } as const; export const TEST_GEOGRAPHY_IDS = { - CALIFORNIA: 'geo-us-ca', - NEW_YORK: 'geo-us-ny', + CALIFORNIA: 'ca', + NEW_YORK: 'ny', } as const; export const TEST_SIMULATION_IDS = { @@ -91,19 +91,13 @@ export const mockHouseholdSinglePerson: Household = { // Mock Geographies export const mockGeographyCalifornia: Geography = { - id: TEST_GEOGRAPHY_IDS.CALIFORNIA, countryId: 'us', - scope: 'subnational', - geographyId: 'ca', - name: 'California', + regionCode: 'ca', }; export const mockGeographyNewYork: Geography = { - id: TEST_GEOGRAPHY_IDS.NEW_YORK, countryId: 'us', - scope: 'subnational', - geographyId: 'ny', - name: 'New York', + regionCode: 'ny', }; // Mock Simulations diff --git a/app/src/tests/fixtures/pages/report-output/SocietyWideReportOutput.ts b/app/src/tests/fixtures/pages/report-output/SocietyWideReportOutput.ts index 3a96167ed..fc785c768 100644 --- a/app/src/tests/fixtures/pages/report-output/SocietyWideReportOutput.ts +++ b/app/src/tests/fixtures/pages/report-output/SocietyWideReportOutput.ts @@ -79,10 +79,8 @@ export const MOCK_USER_POLICY: UserPolicy = { * Mock Geographies for SocietyWideReportOutput tests */ export const MOCK_GEOGRAPHY: Geography = { - id: 'us-us', countryId: 'us', - scope: 'national', - geographyId: 'us', + regionCode: 'us', }; /** diff --git a/app/src/tests/fixtures/pathways/report/views/PopulationViewMocks.ts b/app/src/tests/fixtures/pathways/report/views/PopulationViewMocks.ts index d269f8aaf..b1860e154 100644 --- a/app/src/tests/fixtures/pathways/report/views/PopulationViewMocks.ts +++ b/app/src/tests/fixtures/pathways/report/views/PopulationViewMocks.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { PopulationStateProps } from '@/types/pathwayState'; export const TEST_POPULATION_LABEL = 'Test Population'; @@ -34,11 +35,8 @@ export const mockPopulationStateWithGeography: PopulationStateProps = { type: 'geography', household: null, geography: { - id: 'us-us', countryId: 'us', - geographyId: 'us', - scope: 'national', - name: 'United States', + regionCode: 'us', }, }; diff --git a/app/src/tests/fixtures/utils/populationCompatibilityMocks.ts b/app/src/tests/fixtures/utils/populationCompatibilityMocks.ts index bab5e3e17..590b3360c 100644 --- a/app/src/tests/fixtures/utils/populationCompatibilityMocks.ts +++ b/app/src/tests/fixtures/utils/populationCompatibilityMocks.ts @@ -40,20 +40,14 @@ export function mockPopulationWithHousehold(householdId: string): Population { /** * Creates a mock population with a geography */ -export function mockPopulationWithGeography( - name: string | undefined, - geographyId: string -): Population { +export function mockPopulationWithGeography(regionCode: string): Population { return { label: null, isCreated: true, household: null, geography: { - id: geographyId, countryId: 'us', - scope: 'subnational', - geographyId, - name, + regionCode, }, }; } diff --git a/app/src/tests/fixtures/utils/populationCopyMocks.ts b/app/src/tests/fixtures/utils/populationCopyMocks.ts index 4c92e4849..aa09363da 100644 --- a/app/src/tests/fixtures/utils/populationCopyMocks.ts +++ b/app/src/tests/fixtures/utils/populationCopyMocks.ts @@ -54,11 +54,8 @@ export function mockPopulationWithGeography(): Population { isCreated: true, household: null, geography: { - id: 'us-ca', countryId: 'us', - scope: 'subnational', - geographyId: 'ca', - name: 'California', + regionCode: 'ca', }, }; } diff --git a/app/src/tests/unit/hooks/useStartCalculationOnLoad.test.tsx b/app/src/tests/unit/hooks/useStartCalculationOnLoad.test.tsx index 546339d3c..954dee1b3 100644 --- a/app/src/tests/unit/hooks/useStartCalculationOnLoad.test.tsx +++ b/app/src/tests/unit/hooks/useStartCalculationOnLoad.test.tsx @@ -60,10 +60,8 @@ describe('useStartCalculationOnLoad', () => { household1: null, household2: null, geography1: { - id: 'us-us', countryId: CACHE_HYDRATION_TEST_CONSTANTS.COUNTRY_IDS.US, - scope: 'national', - geographyId: 'us', + regionCode: 'us', }, geography2: null, }, diff --git a/app/src/tests/unit/hooks/useUserReports.test.tsx b/app/src/tests/unit/hooks/useUserReports.test.tsx index 9a1cb98f9..52ca01767 100644 --- a/app/src/tests/unit/hooks/useUserReports.test.tsx +++ b/app/src/tests/unit/hooks/useUserReports.test.tsx @@ -733,11 +733,9 @@ describe('useUserReportById', () => { expect(result.current.geographies).toBeDefined(); expect(result.current.geographies.length).toBeGreaterThan(0); - const geography = result.current.geographies.find((g) => g.geographyId === 'state/ca'); + const geography = result.current.geographies.find((g) => g.regionCode === 'state/ca'); expect(geography).toBeDefined(); - expect(geography?.name).toBe('California'); expect(geography?.countryId).toBe('us'); - expect(geography?.scope).toBe('subnational'); }); test('given geography simulation with no matching region data then geographies array is empty', async () => { @@ -770,7 +768,7 @@ describe('useUserReportById', () => { // Should have an empty geographies array or no geography for the nonexistent region expect(result.current.geographies).toBeDefined(); const nonexistentGeo = result.current.geographies.find( - (g) => g.geographyId === 'nonexistent-region' + (g) => g.regionCode === 'nonexistent-region' ); expect(nonexistentGeo).toBeUndefined(); }); diff --git a/app/src/tests/unit/pages/report-output/GeographySubPage.test.tsx b/app/src/tests/unit/pages/report-output/GeographySubPage.test.tsx index 8cb1ad664..86b0b6bde 100644 --- a/app/src/tests/unit/pages/report-output/GeographySubPage.test.tsx +++ b/app/src/tests/unit/pages/report-output/GeographySubPage.test.tsx @@ -75,9 +75,9 @@ describe('GeographySubPage - Design 4 Table Format', () => { expect(screen.getByText(/geographic area/i)).toBeInTheDocument(); expect(screen.getByText(/type/i)).toBeInTheDocument(); - // Should display values - expect(screen.getByText('California')).toBeInTheDocument(); - expect(screen.getByText('New York')).toBeInTheDocument(); + // Should display regionCode values (Phase 6.2 will add proper name lookup) + expect(screen.getByText('ca')).toBeInTheDocument(); + expect(screen.getByText('ny')).toBeInTheDocument(); expect(screen.getAllByText('Subnational')).toHaveLength(2); // One for baseline, one for reform }); @@ -89,9 +89,9 @@ describe('GeographySubPage - Design 4 Table Format', () => { /> ); - // Should only show California once per row - const californiaElements = screen.getAllByText('California'); - expect(californiaElements.length).toBe(1); + // Should only show regionCode once per row + const caElements = screen.getAllByText('ca'); + expect(caElements.length).toBe(1); }); }); diff --git a/app/src/tests/unit/pages/report-output/PopulationSubPage.test.tsx b/app/src/tests/unit/pages/report-output/PopulationSubPage.test.tsx index 15323c055..8d6aafe17 100644 --- a/app/src/tests/unit/pages/report-output/PopulationSubPage.test.tsx +++ b/app/src/tests/unit/pages/report-output/PopulationSubPage.test.tsx @@ -55,11 +55,9 @@ describe('PopulationSubPage - Design 4 Router', () => { const props = createPopulationSubPageProps.geographyDifferent(); render(); - // Should display California (baseline) - expect(screen.getByText('California')).toBeInTheDocument(); - - // Should display New York (reform) - expect(screen.getByText('New York')).toBeInTheDocument(); + // Should display regionCodes (note: Phase 6.2 will add proper name lookup) + expect(screen.getByText('ca')).toBeInTheDocument(); // Baseline + expect(screen.getByText('ny')).toBeInTheDocument(); // Reform }); test('given missing household data then displays error in HouseholdSubPage', () => { diff --git a/app/src/tests/unit/utils/geographyUtils.test.ts b/app/src/tests/unit/utils/geographyUtils.test.ts index 0b3d262b5..d7432d2b8 100644 --- a/app/src/tests/unit/utils/geographyUtils.test.ts +++ b/app/src/tests/unit/utils/geographyUtils.test.ts @@ -272,10 +272,8 @@ describe('geographyUtils', () => { it('given UK national geography then returns national', () => { // Given const geography: Geography = { - id: 'uk-uk', countryId: 'uk', - scope: 'national', - geographyId: 'uk', + regionCode: 'uk', }; // When @@ -288,10 +286,8 @@ describe('geographyUtils', () => { it('given UK country-level geography then returns country', () => { // Given const geography: Geography = { - id: 'uk-england', countryId: 'uk', - scope: 'subnational', - geographyId: 'country/england', + regionCode: 'country/england', }; // When @@ -304,10 +300,8 @@ describe('geographyUtils', () => { it('given UK constituency geography then returns constituency', () => { // Given const geography: Geography = { - id: 'uk-sheffield-central', countryId: 'uk', - scope: 'subnational', - geographyId: 'constituency/Sheffield Central', + regionCode: 'constituency/Sheffield Central', }; // When @@ -320,10 +314,8 @@ describe('geographyUtils', () => { it('given UK local authority geography then returns local_authority', () => { // Given const geography: Geography = { - id: 'uk-manchester', countryId: 'uk', - scope: 'subnational', - geographyId: 'local_authority/Manchester', + regionCode: 'local_authority/Manchester', }; // When @@ -336,10 +328,8 @@ describe('geographyUtils', () => { it('given US geography then returns null', () => { // Given const geography: Geography = { - id: 'us-ca', countryId: 'us', - scope: 'subnational', - geographyId: 'state/ca', + regionCode: 'state/ca', }; // When @@ -352,10 +342,8 @@ describe('geographyUtils', () => { it('given UK geography with unknown prefix then returns null', () => { // Given const geography: Geography = { - id: 'uk-unknown', countryId: 'uk', - scope: 'subnational', - geographyId: 'unknown/region', + regionCode: 'unknown/region', }; // When @@ -370,10 +358,8 @@ describe('geographyUtils', () => { it('given UK national geography then returns false', () => { // Given const geography: Geography = { - id: 'uk-uk', countryId: 'uk', - scope: 'national', - geographyId: 'uk', + regionCode: 'uk', }; // When @@ -386,10 +372,8 @@ describe('geographyUtils', () => { it('given UK country-level geography then returns false', () => { // Given const geography: Geography = { - id: 'uk-england', countryId: 'uk', - scope: 'subnational', - geographyId: 'country/england', + regionCode: 'country/england', }; // When @@ -402,10 +386,8 @@ describe('geographyUtils', () => { it('given UK constituency geography then returns true', () => { // Given const geography: Geography = { - id: 'uk-sheffield-central', countryId: 'uk', - scope: 'subnational', - geographyId: 'constituency/Sheffield Central', + regionCode: 'constituency/Sheffield Central', }; // When @@ -418,10 +400,8 @@ describe('geographyUtils', () => { it('given UK local authority geography then returns true', () => { // Given const geography: Geography = { - id: 'uk-manchester', countryId: 'uk', - scope: 'subnational', - geographyId: 'local_authority/Manchester', + regionCode: 'local_authority/Manchester', }; // When @@ -434,10 +414,8 @@ describe('geographyUtils', () => { it('given US geography then returns false', () => { // Given const geography: Geography = { - id: 'us-ca', countryId: 'us', - scope: 'subnational', - geographyId: 'state/ca', + regionCode: 'state/ca', }; // When diff --git a/app/src/tests/unit/utils/populationCompatibility.test.ts b/app/src/tests/unit/utils/populationCompatibility.test.ts index f0a43f7fe..c33d34dfb 100644 --- a/app/src/tests/unit/utils/populationCompatibility.test.ts +++ b/app/src/tests/unit/utils/populationCompatibility.test.ts @@ -106,25 +106,14 @@ describe('populationCompatibility', () => { expect(result).toBe(`Household #${TEST_POPULATION_IDS.HOUSEHOLD_1}`); }); - it('given population with geography name but no label then returns geography name', () => { + it('given population with geography but no label then returns regionCode', () => { // Given - const population = mockPopulationWithGeography('California', 'us-ca'); + const population = mockPopulationWithGeography('us-ca'); // When const result = getPopulationLabel(population); - // Then - expect(result).toBe('California'); - }); - - it('given population with geography ID but no name then returns geography ID', () => { - // Given - const population = mockPopulationWithGeography(undefined, 'us-ca'); - - // When - const result = getPopulationLabel(population); - - // Then + // Then - Note: With simplified Geography type, regionCode is returned as fallback expect(result).toBe('us-ca'); }); diff --git a/app/src/types/ingredients/Geography.ts b/app/src/types/ingredients/Geography.ts index 282ff824f..4f32b8550 100644 --- a/app/src/types/ingredients/Geography.ts +++ b/app/src/types/ingredients/Geography.ts @@ -1,16 +1,22 @@ import { countryIds } from '@/libs/countries'; /** - * Base Geography type representing a geographic area for simulation - * Unlike Household, this is validation-only and doesn't require API persistence + * Simplified Geography type representing a geographic area for simulation. + * Uses V2 API region codes directly - the regionCode serves as the identifier. + * + * Region code format: + * - National: country code ("us", "uk") + * - US subnational: "state/ca", "congressional_district/CA-01" + * - UK subnational: "country/england", "constituency/Sheffield Central", "local_authority/..." */ export interface Geography { - id: string; // Format: "{countryId}-{geographyId}" e.g., "us-california" or "uk" for national countryId: (typeof countryIds)[number]; - scope: 'national' | 'subnational'; - geographyId: string; // The geographic identifier from metadata options - // For UK: ALWAYS includes prefix ("constituency/Sheffield Central", "country/england") - // For US: NO prefix (just state code like "ca", "ny") - // National: Just country code ("uk", "us") - name?: string; // Human-readable name + regionCode: string; // V2 API region code - serves as both identifier and API parameter +} + +/** + * Helper to check if a geography represents a national scope + */ +export function isNationalGeography(geography: Geography): boolean { + return geography.regionCode === geography.countryId; } diff --git a/app/src/types/pathwayState/PopulationStateProps.ts b/app/src/types/pathwayState/PopulationStateProps.ts index 2f44596a6..9f02f56b6 100644 --- a/app/src/types/pathwayState/PopulationStateProps.ts +++ b/app/src/types/pathwayState/PopulationStateProps.ts @@ -11,7 +11,7 @@ import { Household } from '@/types/ingredients/Household'; * Can contain either a Household or Geography, but not both. * The `type` field helps track which population type is being managed. * - * Configuration state is determined by presence of `household.id` or `geography.id`. + * Configuration state is determined by presence of `household.id` or `geography.regionCode`. * Use `isPopulationConfigured()` utility to check if population is ready for use. */ export interface PopulationStateProps { diff --git a/app/src/utils/PopulationOps.ts b/app/src/utils/PopulationOps.ts index 71d5693d3..ce6aaf6d8 100644 --- a/app/src/utils/PopulationOps.ts +++ b/app/src/utils/PopulationOps.ts @@ -10,7 +10,7 @@ export type HouseholdPopulationRef = { export type GeographyPopulationRef = { type: 'geography'; - geographyId: string; + regionCode: string; }; export type PopulationRef = HouseholdPopulationRef | GeographyPopulationRef; @@ -44,7 +44,7 @@ export const PopulationOps = { getId: (p: PopulationRef): string => matchPopulation(p, { household: (h) => h.householdId, - geography: (g) => g.geographyId, + geography: (g) => g.regionCode, }), /** @@ -53,7 +53,7 @@ export const PopulationOps = { getLabel: (p: PopulationRef): string => matchPopulation(p, { household: (h) => `Household ${h.householdId}`, - geography: (g) => `All households in ${g.geographyId}`, + geography: (g) => `All households in ${g.regionCode}`, }), /** @@ -77,8 +77,7 @@ export const PopulationOps = { }) as Record, geography: (g) => ({ - geography_id: g.geographyId, - region: g.geographyId, // Some APIs might expect 'region' instead + region: g.regionCode, // V2 API uses 'region' parameter }) as Record, }), @@ -88,7 +87,7 @@ export const PopulationOps = { getCacheKey: (p: PopulationRef): string => matchPopulation(p, { household: (h) => `household:${h.householdId}`, - geography: (g) => `geography:${g.geographyId}`, + geography: (g) => `geography:${g.regionCode}`, }), /** @@ -97,7 +96,7 @@ export const PopulationOps = { isValid: (p: PopulationRef): boolean => matchPopulation(p, { household: (h) => !!h.householdId && h.householdId.length > 0, - geography: (g) => !!g.geographyId && g.geographyId.length > 0, + geography: (g) => !!g.regionCode && g.regionCode.length > 0, }), /** @@ -129,9 +128,9 @@ export const PopulationOps = { /** * Create a geography population reference */ - geography: (geographyId: string): GeographyPopulationRef => ({ + geography: (regionCode: string): GeographyPopulationRef => ({ type: 'geography', - geographyId, + regionCode, }), }; diff --git a/app/src/utils/geographyUtils.ts b/app/src/utils/geographyUtils.ts index 85ae234e2..2a216c485 100644 --- a/app/src/utils/geographyUtils.ts +++ b/app/src/utils/geographyUtils.ts @@ -1,4 +1,4 @@ -import type { Geography } from '@/types/ingredients/Geography'; +import type { Geography, isNationalGeography } from '@/types/ingredients/Geography'; import { MetadataRegionEntry } from '@/types/metadata'; import { UK_REGION_TYPES } from '@/types/regionTypes'; @@ -32,8 +32,11 @@ const KNOWN_PREFIXES = [ 'local_authority', ]; +// Re-export for convenience +export { isNationalGeography }; + /** - * Extracts the UK region type from a Geography object based on its geographyId. + * Extracts the UK region type from a Geography object based on its regionCode. * Returns the region type constant or null if not a UK geography. * * @param geography - The Geography object to analyze @@ -46,23 +49,23 @@ export function getUKRegionTypeFromGeography( return null; } - const { geographyId } = geography; + const { regionCode } = geography; - // National: geographyId equals country code - if (geographyId === 'uk') { + // National: regionCode equals country code + if (regionCode === 'uk') { return UK_REGION_TYPES.NATIONAL; } // Check prefixes for subnational types - if (geographyId.startsWith('country/')) { + if (regionCode.startsWith('country/')) { return UK_REGION_TYPES.COUNTRY; } - if (geographyId.startsWith('constituency/')) { + if (regionCode.startsWith('constituency/')) { return UK_REGION_TYPES.CONSTITUENCY; } - if (geographyId.startsWith('local_authority/')) { + if (regionCode.startsWith('local_authority/')) { return UK_REGION_TYPES.LOCAL_AUTHORITY; } diff --git a/app/src/utils/ingredientReconstruction/convertSimulationStateToApi.ts b/app/src/utils/ingredientReconstruction/convertSimulationStateToApi.ts index 9c85f394b..a90a14170 100644 --- a/app/src/utils/ingredientReconstruction/convertSimulationStateToApi.ts +++ b/app/src/utils/ingredientReconstruction/convertSimulationStateToApi.ts @@ -35,8 +35,9 @@ export function convertSimulationStateToApi( if (population?.household?.id) { populationId = population.household.id; populationType = 'household'; - } else if (population?.geography?.id) { - populationId = population.geography.id; + } else if (population?.geography?.regionCode) { + // For geography, use regionCode as the population identifier + populationId = population.geography.regionCode; populationType = 'geography'; } diff --git a/app/src/utils/ingredientReconstruction/reconstructPopulation.ts b/app/src/utils/ingredientReconstruction/reconstructPopulation.ts index c4207eca3..79765f695 100644 --- a/app/src/utils/ingredientReconstruction/reconstructPopulation.ts +++ b/app/src/utils/ingredientReconstruction/reconstructPopulation.ts @@ -28,19 +28,17 @@ export function reconstructPopulationFromHousehold( * Reconstructs a PopulationStateProps object from a geography * Used when loading existing geographic populations in pathways * - * @param geographyId - The geography ID - * @param geography - The geography data + * @param geography - The geography data (contains countryId and regionCode) * @param label - The population label * @returns A fully-formed PopulationStateProps object */ export function reconstructPopulationFromGeography( - geographyId: string, geography: Geography, label: string | null ): PopulationStateProps { return { household: null, - geography: { ...geography, id: geographyId }, + geography, label, type: 'geography', }; diff --git a/app/src/utils/pathwayCallbacks/populationCallbacks.ts b/app/src/utils/pathwayCallbacks/populationCallbacks.ts index 2bb4c8495..0a4dac82d 100644 --- a/app/src/utils/pathwayCallbacks/populationCallbacks.ts +++ b/app/src/utils/pathwayCallbacks/populationCallbacks.ts @@ -55,7 +55,7 @@ export function createPopulationCallbacks( // If custom completion handler is provided, use it (for standalone pathways) // Otherwise navigate to return mode (for report/simulation pathways) if (onPopulationComplete?.onGeographyComplete) { - onPopulationComplete.onGeographyComplete(geography.geographyId, label); + onPopulationComplete.onGeographyComplete(geography.regionCode, label); } else { navigateToMode(returnMode); } @@ -91,11 +91,11 @@ export function createPopulationCallbacks( ); const handleSelectExistingGeography = useCallback( - (geographyId: string, geography: Geography, label: string) => { + (regionCode: string, geography: Geography, label: string) => { setState((prev) => populationUpdater(prev, { household: null, - geography: { ...geography, id: geographyId }, + geography: { ...geography, regionCode }, label, type: 'geography', }) @@ -133,20 +133,19 @@ export function createPopulationCallbacks( ); const handleGeographicSubmitSuccess = useCallback( - (geographyId: string, label: string) => { + (regionCode: string, label: string) => { setState((prev) => { const population = populationSelector(prev); const updatedPopulation = { ...population }; - if (updatedPopulation.geography) { - updatedPopulation.geography.id = geographyId; - } + // regionCode should already be set on the geography from handleScopeSelected + // Just update the label here updatedPopulation.label = label; return populationUpdater(prev, updatedPopulation); }); // Use custom navigation if provided, otherwise use default if (onPopulationComplete?.onGeographyComplete) { - onPopulationComplete.onGeographyComplete(geographyId, label); + onPopulationComplete.onGeographyComplete(regionCode, label); } else { navigateToMode(returnMode); } diff --git a/app/src/utils/populationCompatibility.ts b/app/src/utils/populationCompatibility.ts index d121ef329..04b5060eb 100644 --- a/app/src/utils/populationCompatibility.ts +++ b/app/src/utils/populationCompatibility.ts @@ -24,7 +24,10 @@ export function arePopulationsCompatible( /** * Gets a human-readable label for a population. - * Priority: population.label → household ID → geography name → 'Unknown Household(s)' + * Priority: population.label → household ID → geography regionCode → 'Unknown Household(s)' + * + * Note: For proper display of geography labels, use getRegionLabel() from geographyUtils + * with region metadata. This function is a fallback when metadata is not available. * * @param population - The population object * @returns A human-readable label @@ -44,14 +47,10 @@ export function getPopulationLabel(population: Population | null): string { return `Household #${population.household.id}`; } - // Third priority: geography name - if (population.geography?.name) { - return population.geography.name; - } - - // Fourth priority: geography ID - if (population.geography?.id) { - return population.geography.id; + // Third priority: geography region code + // TODO: Phase 6.2 will add proper lookup from region metadata + if (population.geography?.regionCode) { + return population.geography.regionCode; } return 'Unknown Household(s)'; diff --git a/app/src/utils/regionStrategies.ts b/app/src/utils/regionStrategies.ts index 4251c1ec6..946d5cd22 100644 --- a/app/src/utils/regionStrategies.ts +++ b/app/src/utils/regionStrategies.ts @@ -159,24 +159,17 @@ export function createGeographyFromScope( scope: ScopeType, countryId: (typeof countryIds)[number], selectedRegion?: string -): { - id: string; - countryId: (typeof countryIds)[number]; - scope: 'national' | 'subnational'; - geographyId: string; -} | null { +): { countryId: (typeof countryIds)[number]; regionCode: string } | null { // Household scope doesn't create geography if (scope === 'household') { return null; } - // National scope uses country ID + // National scope uses country ID as region code if (scope === US_REGION_TYPES.NATIONAL) { return { - id: countryId, countryId, - scope: 'national', - geographyId: countryId, + regionCode: countryId, }; } @@ -185,17 +178,11 @@ export function createGeographyFromScope( return null; } - // Store the full prefixed value for all regions - // For UK: selectedRegion is "constituency/Sheffield Central" or "country/england" - // For US: selectedRegion is "state/ca" or "congressional_district/CA-01" - // We store the FULL value with prefix - - const displayValue = extractRegionDisplayValue(selectedRegion); - + // Region code is the full prefixed value from V2 API + // For UK: "constituency/Sheffield Central", "country/england" + // For US: "state/ca", "congressional_district/CA-01" return { - id: `${countryId}-${displayValue}`, // ID uses display value for backward compat countryId, - scope: 'subnational', - geographyId: selectedRegion, // STORE FULL PREFIXED VALUE + regionCode: selectedRegion, }; } diff --git a/app/src/utils/validation/ingredientValidation.ts b/app/src/utils/validation/ingredientValidation.ts index 8164cdd47..0ae8e85e5 100644 --- a/app/src/utils/validation/ingredientValidation.ts +++ b/app/src/utils/validation/ingredientValidation.ts @@ -26,7 +26,7 @@ export function isPolicyConfigured(policy: PolicyStateProps | null | undefined): * * A population is considered configured if it has either: * - A household with an ID (from API creation) - * - A geography with an ID (from scope selection via createGeographyFromScope) + * - A geography with a regionCode (from scope selection via createGeographyFromScope) */ export function isPopulationConfigured( population: PopulationStateProps | null | undefined @@ -34,7 +34,7 @@ export function isPopulationConfigured( if (!population) { return false; } - return !!(population.household?.id || population.geography?.id); + return !!(population.household?.id || population.geography?.regionCode); } /** From c2185d6cafb519ec576654c9993999a3b5c53f60 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Fri, 13 Feb 2026 22:16:11 +0100 Subject: [PATCH 06/17] fix: Remove static region data --- app/src/data/static/index.ts | 12 +- app/src/data/static/regions/index.ts | 14 - app/src/data/static/regions/resolver.ts | 77 --- app/src/data/static/regions/types.ts | 42 -- .../data/static/regions/uk/constituencies.ts | 75 -- .../regions/uk/data/constituencies_2024.csv | 651 ------------------ .../uk/data/local_authorities_2021.csv | 361 ---------- .../static/regions/uk/localAuthorities.ts | 74 -- .../regions/us/congressionalDistricts.ts | 203 ------ app/src/data/static/staticRegions.ts | 93 --- app/src/hooks/useStaticMetadata.ts | 32 +- .../unit/hooks/useSaveSharedReport.test.tsx | 16 +- .../unit/hooks/useSharedReportData.test.tsx | 5 +- .../unit/hooks/useStaticMetadata.test.ts | 26 +- .../tests/unit/utils/regionStrategies.test.ts | 36 +- 15 files changed, 35 insertions(+), 1682 deletions(-) delete mode 100644 app/src/data/static/regions/index.ts delete mode 100644 app/src/data/static/regions/resolver.ts delete mode 100644 app/src/data/static/regions/types.ts delete mode 100644 app/src/data/static/regions/uk/constituencies.ts delete mode 100644 app/src/data/static/regions/uk/data/constituencies_2024.csv delete mode 100644 app/src/data/static/regions/uk/data/local_authorities_2021.csv delete mode 100644 app/src/data/static/regions/uk/localAuthorities.ts delete mode 100644 app/src/data/static/regions/us/congressionalDistricts.ts delete mode 100644 app/src/data/static/staticRegions.ts diff --git a/app/src/data/static/index.ts b/app/src/data/static/index.ts index 9c669e463..11e32bfe7 100644 --- a/app/src/data/static/index.ts +++ b/app/src/data/static/index.ts @@ -20,11 +20,6 @@ export { // Basic input fields export { getBasicInputs, US_BASIC_INPUTS, UK_BASIC_INPUTS } from './basicInputs'; -// Static region definitions (states and countries only) -// For full regions including congressional districts, constituencies, etc., -// use the versioned regions module: import { resolveRegions } from '@/data/static/regions' -export { US_REGIONS, UK_REGIONS } from './staticRegions'; - // Modelled policies export { getModelledPolicies, @@ -48,11 +43,10 @@ export { export { getTaxYears, getDateRange } from './taxYears'; /** - * Get all static data for a country (excluding regions) + * Get all static data for a country * - * Regions are handled separately via the versioned regions module - * because they vary by simulation year. Use resolveRegions(countryId, year) - * from '@/data/static/regions' for year-aware region resolution. + * Note: Regions are now fetched from the V2 API via useRegions() hook. + * See @/hooks/useRegions for region data. */ export function getStaticData(countryId: string) { return { diff --git a/app/src/data/static/regions/index.ts b/app/src/data/static/regions/index.ts deleted file mode 100644 index c3f5b7f1e..000000000 --- a/app/src/data/static/regions/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Versioned regions module - * - * Provides access to geographic regions that vary by simulation year. - * Use resolveRegions() to get the correct regions for a given country and year. - */ - -export { resolveRegions, getAvailableVersions } from './resolver'; -export type { ResolvedRegions, RegionVersionMeta, VersionedRegionSet } from './types'; - -// Re-export versioned data for direct access if needed -export { US_CONGRESSIONAL_DISTRICTS } from './us/congressionalDistricts'; -export { UK_CONSTITUENCIES } from './uk/constituencies'; -export { UK_LOCAL_AUTHORITIES } from './uk/localAuthorities'; diff --git a/app/src/data/static/regions/resolver.ts b/app/src/data/static/regions/resolver.ts deleted file mode 100644 index 39045dd30..000000000 --- a/app/src/data/static/regions/resolver.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Region resolver - returns the correct regions for a country and simulation year - */ - -import { MetadataRegionEntry } from '@/types/metadata'; -import { UK_REGIONS, US_REGIONS } from '../staticRegions'; -import { ResolvedRegions } from './types'; -import { UK_CONSTITUENCIES } from './uk/constituencies'; -import { UK_LOCAL_AUTHORITIES } from './uk/localAuthorities'; -import { US_CONGRESSIONAL_DISTRICTS } from './us/congressionalDistricts'; - -/** - * Resolve all regions for a country and simulation year - * - * This function returns the correct set of regions based on: - * - Country (us/uk) - * - Simulation year (determines which version of dynamic regions to use) - * - * Static regions (states, countries) are always included. - * Dynamic regions (congressional districts, constituencies, local authorities) - * are resolved based on the simulation year. - */ -export function resolveRegions(countryId: string, year: number): ResolvedRegions { - switch (countryId) { - case 'us': { - const districtVersion = US_CONGRESSIONAL_DISTRICTS.getVersionForYear(year); - const districts = US_CONGRESSIONAL_DISTRICTS.versions[districtVersion].data; - - return { - regions: [...US_REGIONS, ...districts], - versions: { congressionalDistricts: districtVersion }, - }; - } - - case 'uk': { - const constituencyVersion = UK_CONSTITUENCIES.getVersionForYear(year); - const constituencies = UK_CONSTITUENCIES.versions[constituencyVersion].data; - - const laVersion = UK_LOCAL_AUTHORITIES.getVersionForYear(year); - const localAuthorities = UK_LOCAL_AUTHORITIES.versions[laVersion].data; - - return { - regions: [...UK_REGIONS, ...constituencies, ...localAuthorities], - versions: { - constituencies: constituencyVersion, - localAuthorities: laVersion, - }, - }; - } - - default: - return { regions: [], versions: {} }; - } -} - -/** - * Get available region versions for a country - */ -export function getAvailableVersions(countryId: string): { - congressionalDistricts?: string[]; - constituencies?: string[]; - localAuthorities?: string[]; -} { - switch (countryId) { - case 'us': - return { - congressionalDistricts: Object.keys(US_CONGRESSIONAL_DISTRICTS.versions), - }; - case 'uk': - return { - constituencies: Object.keys(UK_CONSTITUENCIES.versions), - localAuthorities: Object.keys(UK_LOCAL_AUTHORITIES.versions), - }; - default: - return {}; - } -} diff --git a/app/src/data/static/regions/types.ts b/app/src/data/static/regions/types.ts deleted file mode 100644 index 03480757b..000000000 --- a/app/src/data/static/regions/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Types for versioned region data - */ - -import { MetadataRegionEntry } from '@/types/metadata'; - -/** - * Metadata for a specific version of region data - */ -export interface RegionVersionMeta { - version: string; - effectiveFrom: number; // Year the version became effective - effectiveUntil: number | null; // Year the version stopped being effective (null = current) - description: string; - source?: string; -} - -/** - * A versioned set of regions with metadata - */ -export interface VersionedRegionSet { - versions: Record< - string, - { - meta: RegionVersionMeta; - data: MetadataRegionEntry[]; - } - >; - getVersionForYear: (year: number) => string; -} - -/** - * Result from resolving regions for a country and year - */ -export interface ResolvedRegions { - regions: MetadataRegionEntry[]; - versions: { - congressionalDistricts?: string; - constituencies?: string; - localAuthorities?: string; - }; -} diff --git a/app/src/data/static/regions/uk/constituencies.ts b/app/src/data/static/regions/uk/constituencies.ts deleted file mode 100644 index 69a27a67f..000000000 --- a/app/src/data/static/regions/uk/constituencies.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * UK Parliamentary Constituencies (2024 Boundaries) - * - * 650 constituencies total - * Effective from 2024 General Election onwards - * - * Data source: policyengine-api/data/constituencies_2024.csv - * Source: UK Boundary Commission reviews - */ - -import { MetadataRegionEntry } from '@/types/metadata'; -import { UK_REGION_TYPES } from '@/types/regionTypes'; -import { RegionVersionMeta, VersionedRegionSet } from '../types'; -import constituenciesCSV from './data/constituencies_2024.csv?raw'; - -/** - * Parse CSV data into constituency entries - * CSV format: code,name,x,y - */ -function parseConstituencies(csv: string): MetadataRegionEntry[] { - const lines = csv.trim().split('\n').slice(1); // Skip header - const constituencies: MetadataRegionEntry[] = []; - - for (const line of lines) { - if (!line.trim()) { - continue; - } - - // Handle CSV with potential quoted fields containing commas - let name: string; - const parts = line.split(','); - - if (line.includes('"')) { - // Find the quoted name - const quoteStart = line.indexOf('"'); - const quoteEnd = line.lastIndexOf('"'); - name = line.substring(quoteStart + 1, quoteEnd); - } else { - // Simple case: code,name,x,y - name = parts[1]; - } - - constituencies.push({ - name: `constituency/${name}`, - label: name, - type: UK_REGION_TYPES.CONSTITUENCY, - }); - } - - return constituencies.sort((a, b) => a.label.localeCompare(b.label)); -} - -const VERSION_2024_BOUNDARIES: RegionVersionMeta = { - version: '2024-boundaries', - effectiveFrom: 2024, - effectiveUntil: null, - description: 'New constituency boundaries effective from 2024 General Election', - source: 'https://www.legislation.gov.uk/uksi/2023/1230/contents/made', -}; - -// Parse constituencies once at module load -const CONSTITUENCIES_2024 = parseConstituencies(constituenciesCSV); - -export const UK_CONSTITUENCIES: VersionedRegionSet = { - versions: { - '2024-boundaries': { - meta: VERSION_2024_BOUNDARIES, - data: CONSTITUENCIES_2024, - }, - }, - getVersionForYear: (_year: number): string => { - // 2024 boundaries are currently the only version - return '2024-boundaries'; - }, -}; diff --git a/app/src/data/static/regions/uk/data/constituencies_2024.csv b/app/src/data/static/regions/uk/data/constituencies_2024.csv deleted file mode 100644 index bd9a1df28..000000000 --- a/app/src/data/static/regions/uk/data/constituencies_2024.csv +++ /dev/null @@ -1,651 +0,0 @@ -code,name,x,y -E14001063,Aldershot,56,-40 -E14001064,Aldridge-Brownhills,56,-30 -E14001065,Altrincham and Sale West,52,-25 -E14001066,Amber Valley,58,-27 -E14001067,Arundel and South Downs,61,-44 -E14001068,Ashfield,60,-27 -E14001069,Ashford,72,-42 -E14001070,Ashton-under-Lyne,54,-23 -E14001071,Aylesbury,60,-35 -E14001072,Banbury,58,-33 -E14001073,Barking,68,-38 -E14001074,Barnsley North,57,-23 -E14001075,Barnsley South,58,-23 -E14001076,Barrow and Furness,54,-16 -E14001077,Basildon and Billericay,67,-34 -E14001078,Basingstoke,55,-39 -E14001079,Bassetlaw,61,-26 -E14001080,Bath,51,-40 -E14001081,Battersea,62,-41 -E14001082,Beaconsfield,57,-37 -E14001083,Beckenham and Penge,65,-43 -E14001084,Bedford,63,-32 -E14001085,Bermondsey and Old Southwark,64,-40 -E14001086,Bethnal Green and Stepney,65,-39 -E14001087,Beverley and Holderness,64,-22 -E14001088,Bexhill and Battle,70,-44 -E14001089,Bexleyheath and Crayford,67,-39 -E14001090,Bicester and Woodstock,59,-34 -E14001091,Birkenhead,49,-27 -E14001092,Birmingham Edgbaston,53,-33 -E14001093,Birmingham Erdington,54,-31 -E14001094,Birmingham Hall Green and Moseley,55,-32 -E14001095,Birmingham Hodge Hill and Solihull North,55,-31 -E14001096,Birmingham Ladywood,54,-32 -E14001097,Birmingham Northfield,54,-34 -E14001098,Birmingham Perry Barr,53,-31 -E14001099,Birmingham Selly Oak,54,-33 -E14001100,Birmingham Yardley,56,-32 -E14001101,Bishop Auckland,54,-14 -E14001102,Blackburn,53,-19 -E14001103,Blackley and Middleton South,53,-23 -E14001104,Blackpool North and Fleetwood,53,-18 -E14001105,Blackpool South,52,-18 -E14001106,Blaydon and Consett,55,-14 -E14001107,Blyth and Ashington,55,-12 -E14001108,Bognor Regis and Littlehampton,63,-44 -E14001109,Bolsover,60,-26 -E14001110,Bolton North East,52,-21 -E14001111,Bolton South and Walkden,52,-22 -E14001112,Bolton West,51,-21 -E14001113,Bootle,49,-22 -E14001114,Boston and Skegness,64,-26 -E14001115,Bournemouth East,52,-43 -E14001116,Bournemouth West,52,-42 -E14001117,Bracknell,56,-39 -E14001118,Bradford East,58,-20 -E14001119,Bradford South,56,-21 -E14001120,Bradford West,57,-20 -E14001121,Braintree,67,-31 -E14001122,Brent East,61,-38 -E14001123,Brent West,60,-38 -E14001124,Brentford and Isleworth,60,-40 -E14001125,Brentwood and Ongar,66,-33 -E14001126,Bridgwater,48,-41 -E14001127,Bridlington and The Wolds,63,-20 -E14001128,Brigg and Immingham,62,-24 -E14001129,Brighton Kemptown and Peacehaven,67,-45 -E14001130,Brighton Pavilion,67,-44 -E14001131,Bristol Central,51,-38 -E14001132,Bristol East,52,-38 -E14001133,Bristol North East,51,-37 -E14001134,Bristol North West,50,-38 -E14001135,Bristol South,51,-39 -E14001136,Broadland and Fakenham,66,-27 -E14001137,Bromley and Biggin Hill,67,-42 -E14001138,Bromsgrove,52,-33 -E14001139,Broxbourne,66,-35 -E14001140,Broxtowe,59,-27 -E14001141,Buckingham and Bletchley,60,-34 -E14001142,Burnley,55,-19 -E14001143,Burton and Uttoxeter,56,-28 -E14001144,Bury North,53,-21 -E14001145,Bury South,53,-22 -E14001146,Bury St Edmunds and Stowmarket,68,-31 -E14001147,Calder Valley,56,-20 -E14001148,Camborne and Redruth,43,-45 -E14001149,Cambridge,65,-30 -E14001150,Cannock Chase,54,-29 -E14001151,Canterbury,71,-41 -E14001152,Carlisle,53,-14 -E14001153,Carshalton and Wallington,62,-43 -E14001154,Castle Point,69,-36 -E14001155,Central Devon,47,-42 -E14001156,Central Suffolk and North Ipswich,68,-29 -E14001157,Chatham and Aylesford,69,-40 -E14001158,Cheadle,55,-26 -E14001159,Chelmsford,67,-33 -E14001160,Chelsea and Fulham,61,-40 -E14001161,Cheltenham,52,-36 -E14001162,Chesham and Amersham,59,-36 -E14001163,Chester North and Neston,50,-28 -E14001164,Chester South and Eddisbury,51,-27 -E14001165,Chesterfield,59,-26 -E14001166,Chichester,60,-44 -E14001167,Chingford and Woodford Green,64,-35 -E14001168,Chippenham,52,-39 -E14001169,Chipping Barnet,62,-36 -E14001170,Chorley,53,-20 -E14001171,Christchurch,53,-42 -E14001172,Cities of London and Westminster,63,-40 -E14001173,City of Durham,55,-16 -E14001174,Clacton,69,-32 -E14001175,Clapham and Brixton Hill,62,-42 -E14001176,Colchester,68,-32 -E14001177,Colne Valley,55,-23 -E14001178,Congleton,54,-27 -E14001179,Corby and East Northamptonshire,62,-30 -E14001180,Coventry East,57,-33 -E14001181,Coventry North West,56,-33 -E14001182,Coventry South,57,-34 -E14001183,Cramlington and Killingworth,56,-12 -E14001184,Crawley,69,-44 -E14001185,Crewe and Nantwich,53,-27 -E14001186,Croydon East,65,-42 -E14001187,Croydon South,64,-43 -E14001188,Croydon West,63,-43 -E14001189,Dagenham and Rainham,67,-37 -E14001190,Darlington,55,-17 -E14001191,Dartford,68,-40 -E14001192,Daventry,60,-32 -E14001193,Derby North,58,-28 -E14001194,Derby South,57,-28 -E14001195,Derbyshire Dales,57,-26 -E14001196,Dewsbury and Batley,57,-22 -E14001197,Didcot and Wantage,54,-38 -E14001198,Doncaster Central,60,-23 -E14001199,Doncaster East and the Isle of Axholme,61,-23 -E14001200,Doncaster North,61,-22 -E14001201,Dorking and Horley,59,-43 -E14001202,Dover and Deal,72,-41 -E14001203,Droitwich and Evesham,54,-36 -E14001204,Dudley,51,-31 -E14001205,Dulwich and West Norwood,63,-42 -E14001206,Dunstable and Leighton Buzzard,62,-33 -E14001207,Ealing Central and Acton,59,-39 -E14001208,Ealing North,59,-38 -E14001209,Ealing Southall,58,-39 -E14001210,Earley and Woodley,56,-36 -E14001211,Easington,57,-16 -E14001212,East Grinstead and Uckfield,69,-43 -E14001213,East Ham,67,-38 -E14001214,East Hampshire,55,-41 -E14001215,East Surrey,67,-43 -E14001216,East Thanet,71,-39 -E14001217,East Wiltshire,53,-41 -E14001218,East Worthing and Shoreham,65,-44 -E14001219,Eastbourne,69,-45 -E14001220,Eastleigh,54,-41 -E14001221,Edmonton and Winchmore Hill,64,-36 -E14001222,Ellesmere Port and Bromborough,50,-27 -E14001223,Eltham and Chislehurst,66,-41 -E14001224,Ely and East Cambridgeshire,66,-30 -E14001225,Enfield North,62,-35 -E14001226,Epping Forest,67,-35 -E14001227,Epsom and Ewell,60,-43 -E14001228,Erewash,59,-28 -E14001229,Erith and Thamesmead,67,-40 -E14001230,Esher and Walton,58,-42 -E14001231,Exeter,48,-42 -E14001232,Exmouth and Exeter East,48,-43 -E14001233,Fareham and Waterlooville,55,-43 -E14001234,Farnham and Bordon,56,-42 -E14001235,Faversham and Mid Kent,71,-40 -E14001236,Feltham and Heston,59,-40 -E14001237,Filton and Bradley Stoke,50,-37 -E14001238,Finchley and Golders Green,61,-37 -E14001239,Folkestone and Hythe,71,-42 -E14001240,Forest of Dean,50,-35 -E14001241,Frome and East Somerset,50,-41 -E14001242,Fylde,51,-19 -E14001243,Gainsborough,61,-25 -E14001244,Gateshead Central and Whickham,56,-15 -E14001245,Gedling,61,-28 -E14001246,Gillingham and Rainham,70,-40 -E14001247,Glastonbury and Somerton,49,-41 -E14001248,Gloucester,51,-35 -E14001249,Godalming and Ash,57,-42 -E14001250,Goole and Pocklington,61,-21 -E14001251,Gorton and Denton,55,-24 -E14001252,Gosport,57,-43 -E14001253,Grantham and Bourne,63,-28 -E14001254,Gravesham,68,-39 -E14001255,Great Grimsby and Cleethorpes,63,-24 -E14001256,Great Yarmouth,67,-27 -E14001257,Greenwich and Woolwich,66,-40 -E14001258,Guildford,56,-41 -E14001259,Hackney North and Stoke Newington,64,-38 -E14001260,Hackney South and Shoreditch,64,-39 -E14001261,Halesowen,51,-33 -E14001262,Halifax,55,-21 -E14001263,Hamble Valley,56,-43 -E14001264,Hammersmith and Chiswick,60,-39 -E14001265,Hampstead and Highgate,62,-38 -E14001266,"Harborough, Oadby and Wigston",61,-31 -E14001267,Harlow,67,-32 -E14001268,Harpenden and Berkhamsted,62,-34 -E14001269,Harrogate and Knaresborough,59,-18 -E14001270,Harrow East,60,-37 -E14001271,Harrow West,59,-37 -E14001272,Hartlepool,59,-16 -E14001273,Harwich and North Essex,69,-31 -E14001274,Hastings and Rye,70,-43 -E14001275,Havant,59,-44 -E14001276,Hayes and Harlington,58,-38 -E14001277,Hazel Grove,55,-25 -E14001278,Hemel Hempstead,64,-34 -E14001279,Hendon,61,-36 -E14001280,Henley and Thame,58,-35 -E14001281,Hereford and South Herefordshire,51,-34 -E14001282,Herne Bay and Sandwich,72,-40 -E14001283,Hertford and Stortford,66,-32 -E14001284,Hertsmere,66,-34 -E14001285,Hexham,53,-13 -E14001286,Heywood and Middleton North,54,-20 -E14001287,High Peak,56,-25 -E14001288,Hinckley and Bosworth,58,-30 -E14001289,Hitchin,64,-32 -E14001290,Holborn and St Pancras,62,-39 -E14001291,Honiton and Sidmouth,49,-43 -E14001292,Hornchurch and Upminster,66,-37 -E14001293,Hornsey and Friern Barnet,63,-36 -E14001294,Horsham,62,-44 -E14001295,Houghton and Sunderland South,57,-15 -E14001296,Hove and Portslade,66,-44 -E14001297,Huddersfield,56,-22 -E14001298,Huntingdon,63,-31 -E14001299,Hyndburn,54,-19 -E14001300,Ilford North,65,-36 -E14001301,Ilford South,65,-37 -E14001302,Ipswich,68,-30 -E14001303,Isle of Wight East,54,-45 -E14001304,Isle of Wight West,53,-45 -E14001305,Islington North,63,-38 -E14001306,Islington South and Finsbury,63,-39 -E14001307,Jarrow and Gateshead East,57,-14 -E14001308,Keighley and Ilkley,56,-19 -E14001309,Kenilworth and Southam,56,-34 -E14001310,Kensington and Bayswater,61,-39 -E14001311,Kettering,61,-30 -E14001312,Kingston and Surbiton,59,-42 -E14001313,Kingston upon Hull East,63,-22 -E14001314,Kingston upon Hull North and Cottingham,62,-21 -E14001315,Kingston upon Hull West and Haltemprice,62,-22 -E14001316,Kingswinford and South Staffordshire,52,-30 -E14001317,Knowsley,50,-23 -E14001318,Lancaster and Wyre,54,-18 -E14001319,Leeds Central and Headingley,60,-20 -E14001320,Leeds East,61,-20 -E14001321,Leeds North East,59,-19 -E14001322,Leeds North West,58,-19 -E14001323,Leeds South,59,-21 -E14001324,Leeds South West and Morley,58,-21 -E14001325,Leeds West and Pudsey,59,-20 -E14001326,Leicester East,60,-30 -E14001327,Leicester South,60,-31 -E14001328,Leicester West,59,-31 -E14001329,Leigh and Atherton,51,-25 -E14001330,Lewes,68,-45 -E14001331,Lewisham East,66,-42 -E14001332,Lewisham North,65,-40 -E14001333,Lewisham West and East Dulwich,65,-41 -E14001334,Leyton and Wanstead,64,-37 -E14001335,Lichfield,56,-29 -E14001336,Lincoln,62,-25 -E14001337,Liverpool Garston,50,-25 -E14001338,Liverpool Riverside,49,-24 -E14001339,Liverpool Walton,49,-23 -E14001340,Liverpool Wavertree,49,-25 -E14001341,Liverpool West Derby,50,-24 -E14001342,Loughborough,59,-30 -E14001343,Louth and Horncastle,63,-25 -E14001344,Lowestoft,68,-28 -E14001345,Luton North,63,-33 -E14001346,Luton South and South Bedfordshire,63,-34 -E14001347,Macclesfield,56,-26 -E14001348,Maidenhead,57,-36 -E14001349,Maidstone and Malling,69,-41 -E14001350,Makerfield,51,-22 -E14001351,Maldon,69,-33 -E14001352,Manchester Central,54,-24 -E14001353,Manchester Rusholme,53,-25 -E14001354,Manchester Withington,54,-26 -E14001355,Mansfield,61,-27 -E14001356,Melksham and Devizes,52,-40 -E14001357,Melton and Syston,61,-29 -E14001358,Meriden and Solihull East,55,-33 -E14001359,Mid Bedfordshire,62,-32 -E14001360,Mid Buckinghamshire,59,-35 -E14001361,Mid Cheshire,52,-27 -E14001362,Mid Derbyshire,57,-27 -E14001363,Mid Dorset and North Poole,50,-43 -E14001364,Mid Leicestershire,58,-31 -E14001365,Mid Norfolk,65,-28 -E14001366,Mid Sussex,68,-43 -E14001367,Middlesbrough and Thornaby East,57,-17 -E14001368,Middlesbrough South and East Cleveland,59,-17 -E14001369,Milton Keynes Central,61,-34 -E14001370,Milton Keynes North,61,-33 -E14001371,Mitcham and Morden,61,-43 -E14001372,Morecambe and Lunesdale,54,-17 -E14001373,New Forest East,54,-43 -E14001374,New Forest West,53,-43 -E14001375,Newark,62,-26 -E14001376,Newbury,54,-37 -E14001377,Newcastle upon Tyne Central and West,54,-13 -E14001378,Newcastle upon Tyne East and Wallsend,56,-14 -E14001379,Newcastle upon Tyne North,55,-13 -E14001380,Newcastle-under-Lyme,52,-28 -E14001381,Newton Abbot,47,-43 -E14001382,Newton Aycliffe and Spennymoor,56,-16 -E14001383,Normanton and Hemsworth,59,-23 -E14001384,North Bedfordshire,62,-31 -E14001385,North Cornwall,45,-43 -E14001386,North Cotswolds,53,-37 -E14001387,North Devon,46,-41 -E14001388,North Dorset,51,-42 -E14001389,North Durham,54,-15 -E14001390,North East Cambridgeshire,64,-29 -E14001391,North East Derbyshire,58,-26 -E14001392,North East Hampshire,56,-38 -E14001393,North East Hertfordshire,65,-32 -E14001394,North East Somerset and Hanham,50,-39 -E14001395,North Herefordshire,52,-34 -E14001396,North Norfolk,65,-27 -E14001397,North Northumberland,54,-12 -E14001398,North Shropshire,50,-29 -E14001399,North Somerset,49,-39 -E14001400,North Warwickshire and Bedworth,57,-32 -E14001401,North West Cambridgeshire,64,-30 -E14001402,North West Essex,66,-31 -E14001403,North West Hampshire,54,-39 -E14001404,North West Leicestershire,58,-29 -E14001405,North West Norfolk,64,-28 -E14001406,Northampton North,61,-32 -E14001407,Northampton South,60,-33 -E14001408,Norwich North,66,-28 -E14001409,Norwich South,66,-29 -E14001410,Nottingham East,60,-29 -E14001411,Nottingham North and Kimberley,60,-28 -E14001412,Nottingham South,59,-29 -E14001413,Nuneaton,57,-31 -E14001414,Old Bexley and Sidcup,67,-41 -E14001415,Oldham East and Saddleworth,55,-22 -E14001416,"Oldham West, Chadderton and Royton",54,-22 -E14001417,Orpington,66,-43 -E14001418,Ossett and Denby Dale,58,-22 -E14001419,Oxford East,58,-34 -E14001420,Oxford West and Abingdon,57,-35 -E14001421,Peckham,64,-41 -E14001422,Pendle and Clitheroe,56,-18 -E14001423,Penistone and Stocksbridge,56,-23 -E14001424,Penrith and Solway,52,-15 -E14001425,Peterborough,63,-29 -E14001426,Plymouth Moor View,46,-43 -E14001427,Plymouth Sutton and Devonport,47,-44 -E14001428,"Pontefract, Castleford and Knottingley",60,-22 -E14001429,Poole,51,-43 -E14001430,Poplar and Limehouse,66,-39 -E14001431,Portsmouth North,58,-43 -E14001432,Portsmouth South,58,-44 -E14001433,Preston,52,-19 -E14001434,Putney,61,-41 -E14001435,Queen's Park and Maida Vale,62,-40 -E14001436,Rawmarsh and Conisbrough,60,-24 -E14001437,Rayleigh and Wickford,68,-34 -E14001438,Reading Central,55,-37 -E14001439,Reading West and Mid Berkshire,55,-36 -E14001440,Redcar,58,-17 -E14001441,Redditch,53,-35 -E14001442,Reigate,68,-44 -E14001443,Ribble Valley,55,-18 -E14001444,Richmond and Northallerton,57,-18 -E14001445,Richmond Park,59,-41 -E14001446,Rochdale,54,-21 -E14001447,Rochester and Strood,69,-39 -E14001448,Romford,66,-36 -E14001449,Romsey and Southampton North,54,-40 -E14001450,Rossendale and Darwen,55,-20 -E14001451,Rother Valley,60,-25 -E14001452,Rotherham,59,-24 -E14001453,Rugby,58,-32 -E14001454,"Ruislip, Northwood and Pinner",60,-36 -E14001455,Runcorn and Helsby,51,-28 -E14001456,Runnymede and Weybridge,57,-41 -E14001457,Rushcliffe,62,-28 -E14001458,Rutland and Stamford,62,-29 -E14001459,Salford,53,-24 -E14001460,Salisbury,52,-41 -E14001461,Scarborough and Whitby,61,-19 -E14001462,Scunthorpe,61,-24 -E14001463,Sefton Central,50,-20 -E14001464,Selby,60,-21 -E14001465,Sevenoaks,68,-42 -E14001466,Sheffield Brightside and Hillsborough,58,-24 -E14001467,Sheffield Central,58,-25 -E14001468,Sheffield Hallam,57,-24 -E14001469,Sheffield Heeley,57,-25 -E14001470,Sheffield South East,59,-25 -E14001471,Sherwood Forest,62,-27 -E14001472,Shipley,57,-19 -E14001473,Shrewsbury,51,-30 -E14001474,Sittingbourne and Sheppey,70,-39 -E14001475,Skipton and Ripon,58,-18 -E14001476,Sleaford and North Hykeham,63,-26 -E14001477,Slough,56,-37 -E14001478,Smethwick,53,-32 -E14001479,Solihull West and Shirley,55,-34 -E14001480,South Basildon and East Thurrock,68,-36 -E14001481,South Cambridgeshire,65,-31 -E14001482,South Cotswolds,53,-38 -E14001483,South Derbyshire,57,-29 -E14001484,South Devon,48,-45 -E14001485,South Dorset,51,-44 -E14001486,South East Cornwall,46,-44 -E14001487,South Holland and The Deepings,63,-27 -E14001488,South Leicestershire,59,-32 -E14001489,South Norfolk,67,-29 -E14001490,South Northamptonshire,59,-33 -E14001491,South Ribble,52,-20 -E14001492,South Shields,58,-14 -E14001493,South Shropshire,50,-31 -E14001494,South Suffolk,69,-30 -E14001495,South West Devon,47,-45 -E14001496,South West Hertfordshire,61,-35 -E14001497,South West Norfolk,65,-29 -E14001498,South West Wiltshire,51,-41 -E14001499,Southampton Itchen,55,-42 -E14001500,Southampton Test,54,-42 -E14001501,Southend East and Rochford,69,-34 -E14001502,Southend West and Leigh,68,-35 -E14001503,Southgate and Wood Green,63,-35 -E14001504,Southport,50,-19 -E14001505,Spelthorne,58,-40 -E14001506,Spen Valley,57,-21 -E14001507,St Albans,65,-34 -E14001508,St Austell and Newquay,45,-44 -E14001509,St Helens North,50,-21 -E14001510,St Helens South and Whiston,50,-22 -E14001511,St Ives,43,-46 -E14001512,St Neots and Mid Cambridgeshire,64,-31 -E14001513,Stafford,54,-28 -E14001514,Staffordshire Moorlands,56,-27 -E14001515,Stalybridge and Hyde,56,-24 -E14001516,Stevenage,64,-33 -E14001517,Stockport,54,-25 -E14001518,Stockton North,58,-16 -E14001519,Stockton West,56,-17 -E14001520,Stoke-on-Trent Central,55,-28 -E14001521,Stoke-on-Trent North,55,-27 -E14001522,Stoke-on-Trent South,55,-29 -E14001523,"Stone, Great Wyrley and Penkridge",53,-28 -E14001524,Stourbridge,51,-32 -E14001525,Stratford and Bow,65,-38 -E14001526,Stratford-on-Avon,54,-35 -E14001527,Streatham and Croydon North,64,-42 -E14001528,Stretford and Urmston,52,-24 -E14001529,Stroud,52,-37 -E14001530,Suffolk Coastal,69,-29 -E14001531,Sunderland Central,58,-15 -E14001532,Surrey Heath,57,-39 -E14001533,Sussex Weald,70,-42 -E14001534,Sutton and Cheam,60,-42 -E14001535,Sutton Coldfield,56,-31 -E14001536,Swindon North,53,-39 -E14001537,Swindon South,53,-40 -E14001538,Tamworth,57,-30 -E14001539,Tatton,52,-26 -E14001540,Taunton and Wellington,49,-42 -E14001541,Telford,52,-29 -E14001542,Tewkesbury,53,-36 -E14001543,The Wrekin,51,-29 -E14001544,Thirsk and Malton,60,-18 -E14001545,Thornbury and Yate,51,-36 -E14001546,Thurrock,67,-36 -E14001547,Tipton and Wednesbury,52,-31 -E14001548,Tiverton and Minehead,47,-41 -E14001549,Tonbridge,68,-41 -E14001550,Tooting,61,-42 -E14001551,Torbay,48,-44 -E14001552,Torridge and Tavistock,46,-42 -E14001553,Tottenham,62,-37 -E14001554,Truro and Falmouth,44,-45 -E14001555,Tunbridge Wells,69,-42 -E14001556,Twickenham,58,-41 -E14001557,Tynemouth,56,-13 -E14001558,Uxbridge and South Ruislip,58,-37 -E14001559,Vauxhall and Camberwell Green,63,-41 -E14001560,Wakefield and Rothwell,59,-22 -E14001561,Wallasey,48,-27 -E14001562,Walsall and Bloxwich,55,-30 -E14001563,Walthamstow,63,-37 -E14001564,Warrington North,51,-23 -E14001565,Warrington South,51,-24 -E14001566,Warwick and Leamington,55,-35 -E14001567,Washington and Gateshead South,55,-15 -E14001568,Watford,65,-35 -E14001569,Waveney Valley,67,-28 -E14001570,Weald of Kent,70,-41 -E14001571,Wellingborough and Rushden,63,-30 -E14001572,Wells and Mendip Hills,50,-40 -E14001573,Welwyn Hatfield,65,-33 -E14001574,West Bromwich,52,-32 -E14001575,West Dorset,50,-44 -E14001576,West Ham and Beckton,66,-38 -E14001577,West Lancashire,49,-21 -E14001578,West Suffolk,67,-30 -E14001579,West Worcestershire,52,-35 -E14001580,Westmorland and Lonsdale,53,-15 -E14001581,Weston-super-Mare,49,-40 -E14001582,Wetherby and Easingwold,62,-20 -E14001583,Whitehaven and Workington,53,-16 -E14001584,Widnes and Halewood,51,-26 -E14001585,Wigan,51,-20 -E14001586,Wimbledon,60,-41 -E14001587,Winchester,55,-40 -E14001588,Windsor,57,-38 -E14001589,Wirral West,49,-28 -E14001590,Witham,68,-33 -E14001591,Witney,56,-35 -E14001592,Woking,57,-40 -E14001593,Wokingham,55,-38 -E14001594,Wolverhampton North East,53,-29 -E14001595,Wolverhampton South East,54,-30 -E14001596,Wolverhampton West,53,-30 -E14001597,Worcester,53,-34 -E14001598,Worsley and Eccles,52,-23 -E14001599,Worthing West,64,-44 -E14001600,Wycombe,58,-36 -E14001601,Wyre Forest,50,-33 -E14001602,Wythenshawe and Sale East,53,-26 -E14001603,Yeovil,50,-42 -E14001604,York Central,60,-19 -E14001605,York Outer,61,-18 -N05000001,Belfast East,45,-17 -N05000002,Belfast North,45,-16 -N05000003,Belfast South and Mid Down,45,-18 -N05000004,Belfast West,44,-17 -N05000005,East Antrim,45,-15 -N05000006,East Londonderry,43,-15 -N05000007,Fermanagh and South Tyrone,42,-17 -N05000008,Foyle,42,-15 -N05000009,Lagan Valley,44,-18 -N05000010,Mid Ulster,43,-16 -N05000011,Newry and Armagh,44,-19 -N05000012,North Antrim,44,-15 -N05000013,North Down,46,-16 -N05000014,South Antrim,44,-16 -N05000015,South Down,46,-18 -N05000016,Strangford,46,-17 -N05000017,Upper Bann,43,-18 -N05000018,West Tyrone,42,-16 -S14000021,East Renfrewshire,48,-11 -S14000027,Na h-Eileanan an Iar,47,-2 -S14000045,Midlothian,52,-11 -S14000048,North Ayrshire and Arran,48,-10 -S14000051,Orkney and Shetland,51,0 -S14000060,Aberdeen North,52,-3 -S14000061,Aberdeen South,52,-4 -S14000062,Aberdeenshire North and Moray East,51,-3 -S14000063,Airdrie and Shotts,50,-11 -S14000064,Alloa and Grangemouth,50,-7 -S14000065,Angus and Perthshire Glens,50,-5 -S14000066,Arbroath and Broughty Ferry,52,-5 -S14000067,"Argyll, Bute and South Lochaber",49,-5 -S14000068,Bathgate and Linlithgow,51,-9 -S14000069,"Caithness, Sutherland and Easter Ross",50,-2 -S14000070,Coatbridge and Bellshill,50,-12 -S14000071,Cowdenbeath and Kirkcaldy,52,-7 -S14000072,Cumbernauld and Kirkintilloch,50,-8 -S14000073,Dumfries and Galloway,51,-13 -S14000074,"Dumfriesshire, Clydesdale and Tweeddale",52,-13 -S14000075,Dundee Central,50,-6 -S14000076,Dunfermline and Dollar,51,-7 -S14000077,East Kilbride and Strathaven,48,-13 -S14000078,Edinburgh East and Musselburgh,54,-10 -S14000079,Edinburgh North and Leith,53,-9 -S14000080,Edinburgh South,53,-10 -S14000081,Edinburgh South West,52,-10 -S14000082,Edinburgh West,52,-9 -S14000083,Falkirk,51,-8 -S14000084,Glasgow East,51,-10 -S14000085,Glasgow North,49,-9 -S14000086,Glasgow North East,50,-9 -S14000087,Glasgow South,49,-11 -S14000088,Glasgow South West,50,-10 -S14000089,Glasgow West,49,-8 -S14000090,Glenrothes and Mid Fife,52,-6 -S14000091,Gordon and Buchan,50,-4 -S14000092,Hamilton and Clyde Valley,51,-12 -S14000093,Inverclyde and Renfrewshire West,48,-8 -S14000094,"Inverness, Skye and West Ross-shire",49,-3 -S14000095,Livingston,51,-11 -S14000096,Lothian East,53,-11 -S14000097,Mid Dunbartonshire,49,-7 -S14000098,"Moray West, Nairn and Strathspey",49,-4 -S14000099,"Motherwell, Wishaw and Carluke",52,-12 -S14000100,North East Fife,51,-6 -S14000101,Paisley and Renfrewshire North,48,-9 -S14000102,Paisley and Renfrewshire South,49,-10 -S14000103,Perth and Kinross-shire,51,-5 -S14000104,Rutherglen,49,-12 -S14000105,Stirling and Strathallan,49,-6 -S14000106,West Dunbartonshire,48,-7 -S14000107,"Ayr, Carrick and Cumnock",49,-13 -S14000108,"Berwickshire, Roxburgh and Selkirk",53,-12 -S14000109,Central Ayrshire,48,-12 -S14000110,Kilmarnock and Loudoun,50,-13 -S14000111,West Aberdeenshire and Kincardine,51,-4 -W07000081,Aberafan Maesteg,46,-36 -W07000082,Alyn and Deeside,49,-29 -W07000083,Bangor Aberconwy,47,-31 -W07000084,Blaenau Gwent and Rhymney,49,-33 -W07000085,"Brecon, Radnor and Cwm Tawe",50,-32 -W07000086,Bridgend,46,-37 -W07000087,Caerfyrddin,49,-32 -W07000088,Caerphilly,49,-35 -W07000089,Cardiff East,48,-37 -W07000090,Cardiff North,48,-36 -W07000091,Cardiff South and Penarth,48,-38 -W07000092,Cardiff West,47,-37 -W07000093,Ceredigion Preseli,48,-34 -W07000094,Clwyd East,49,-30 -W07000095,Clwyd North,48,-30 -W07000096,Dwyfor Meirionnydd,48,-31 -W07000097,Gower,44,-37 -W07000098,Llanelli,45,-36 -W07000099,Merthyr Tydfil and Aberdare,49,-34 -W07000100,Mid and South Pembrokeshire,44,-36 -W07000101,Monmouthshire,50,-36 -W07000102,Montgomeryshire and Glyndwr,49,-31 -W07000103,Neath and Swansea East,47,-35 -W07000104,Newport East,49,-37 -W07000105,Newport West and Islwyn,49,-36 -W07000106,Pontypridd,48,-35 -W07000107,Rhondda and Ogmore,47,-36 -W07000108,Swansea West,45,-37 -W07000109,Torfaen,50,-34 -W07000110,Vale of Glamorgan,47,-38 -W07000111,Wrexham,50,-30 -W07000112,Ynys Môn,46,-29 diff --git a/app/src/data/static/regions/uk/data/local_authorities_2021.csv b/app/src/data/static/regions/uk/data/local_authorities_2021.csv deleted file mode 100644 index 9fcf922ed..000000000 --- a/app/src/data/static/regions/uk/data/local_authorities_2021.csv +++ /dev/null @@ -1,361 +0,0 @@ -code,x,y,name -E06000001,8.0,19.0,Hartlepool -E06000002,9.0,18.0,Middlesbrough -E06000003,9.0,19.0,Redcar and Cleveland -E06000004,8.0,18.0,Stockton-on-Tees -E06000005,7.0,18.0,Darlington -E06000006,1.0,11.0,Halton -E06000007,2.0,11.0,Warrington -E06000008,4.0,15.0,Blackburn with Darwen -E06000009,2.0,15.0,Blackpool -E06000010,10.0,15.0,"Kingston upon Hull, City of" -E06000011,11.0,16.0,East Riding of Yorkshire -E06000012,11.0,14.0,North East Lincolnshire -E06000013,10.0,14.0,North Lincolnshire -E06000014,9.0,17.0,York -E06000015,6.0,11.0,Derby -E06000016,8.0,8.0,Leicester -E06000017,10.0,9.0,Rutland -E06000018,8.0,10.0,Nottingham -E06000019,0.0,8.0,"Herefordshire, County of" -E06000020,2.0,9.0,Telford and Wrekin -E06000021,3.0,10.0,Stoke-on-Trent -E06000022,1.0,3.0,Bath and North East Somerset -E06000023,0.0,3.0,"Bristol, City of" -E06000024,0.0,2.0,North Somerset -E06000025,1.0,4.0,South Gloucestershire -E06000026,-4.0,-2.0,Plymouth -E06000027,-3.0,-2.0,Torbay -E06000030,2.0,4.0,Swindon -E06000031,11.0,9.0,Peterborough -E06000032,10.0,7.0,Luton -E06000033,16.0,6.0,Southend-on-Sea -E06000034,15.0,4.0,Thurrock -E06000035,15.0,1.0,Medway -E06000036,4.0,2.0,Bracknell Forest -E06000037,2.0,2.0,West Berkshire -E06000038,2.0,3.0,Reading -E06000039,6.0,4.0,Slough -E06000040,4.0,3.0,Windsor and Maidenhead -E06000041,3.0,3.0,Wokingham -E06000042,6.0,5.0,Milton Keynes -E06000043,9.0,-2.0,Brighton and Hove -E06000044,4.0,-1.0,Portsmouth -E06000045,2.0,0.0,Southampton -E06000046,1.0,-2.0,Isle of Wight -E06000047,6.0,18.0,County Durham -E06000049,4.0,11.0,Cheshire East -E06000050,3.0,11.0,Cheshire West and Chester -E06000051,1.0,9.0,Shropshire -E06000052,-5.0,-2.0,Cornwall -E06000053,-7.0,-3.0,Isles of Scilly -E06000054,1.0,2.0,Wiltshire -E06000055,9.0,7.0,Bedford -E06000056,9.0,6.0,Central Bedfordshire -E06000057,5.0,20.0,Northumberland -E06000058,0.0,0.0,"Bournemouth, Christchurch and Poole" -E06000059,-1.0,0.0,Dorset -E06000060,5.0,5.0,Buckinghamshire -E06000061,9.0,9.0,North Northamptonshire -E06000062,7.0,6.0,West Northamptonshire -E06000063,0.0,0.0,Cumberland -E06000064,0.0,0.0,Westmorland and Furness -E06000065,0.0,0.0,North Yorkshire -E06000066,0.0,0.0,Somerset -E07000008,12.0,8.0,Cambridge -E07000009,12.0,9.0,East Cambridgeshire -E07000010,13.0,10.0,Fenland -E07000011,10.0,8.0,Huntingdonshire -E07000012,11.0,8.0,South Cambridgeshire -E07000032,7.0,11.0,Amber Valley -E07000033,10.0,12.0,Bolsover -E07000034,9.0,12.0,Chesterfield -E07000035,7.0,12.0,Derbyshire Dales -E07000036,7.0,9.0,Erewash -E07000037,7.0,13.0,High Peak -E07000038,8.0,12.0,North East Derbyshire -E07000039,6.0,10.0,South Derbyshire -E07000040,-2.0,-1.0,East Devon -E07000041,-3.0,-1.0,Exeter -E07000042,-2.0,0.0,Mid Devon -E07000043,-3.0,1.0,North Devon -E07000044,-4.0,-3.0,South Hams -E07000045,-2.0,-2.0,Teignbridge -E07000046,-4.0,-1.0,Torridge -E07000047,-3.0,0.0,West Devon -E07000061,10.0,-2.0,Eastbourne -E07000062,13.0,-2.0,Hastings -E07000063,10.0,-1.0,Lewes -E07000064,12.0,-2.0,Rother -E07000065,11.0,-2.0,Wealden -E07000066,14.0,5.0,Basildon -E07000067,14.0,7.0,Braintree -E07000068,13.0,5.0,Brentwood -E07000069,15.0,5.0,Castle Point -E07000070,14.0,6.0,Chelmsford -E07000071,15.0,8.0,Colchester -E07000072,12.0,5.0,Epping Forest -E07000073,13.0,6.0,Harlow -E07000074,15.0,7.0,Maldon -E07000075,15.0,6.0,Rochford -E07000076,16.0,8.0,Tendring -E07000077,13.0,7.0,Uttlesford -E07000078,1.0,5.0,Cheltenham -E07000079,2.0,5.0,Cotswold -E07000080,-1.0,6.0,Forest of Dean -E07000081,0.0,6.0,Gloucester -E07000082,0.0,5.0,Stroud -E07000083,1.0,6.0,Tewkesbury -E07000084,2.0,1.0,Basingstoke and Deane -E07000085,4.0,0.0,East Hampshire -E07000086,3.0,0.0,Eastleigh -E07000087,2.0,-1.0,Fareham -E07000088,3.0,-1.0,Gosport -E07000089,3.0,2.0,Hart -E07000090,5.0,0.0,Havant -E07000091,1.0,0.0,New Forest -E07000092,4.0,1.0,Rushmoor -E07000093,1.0,1.0,Test Valley -E07000094,3.0,1.0,Winchester -E07000095,12.0,6.0,Broxbourne -E07000096,8.0,6.0,Dacorum -E07000098,9.0,5.0,Hertsmere -E07000099,11.0,7.0,North Hertfordshire -E07000102,7.0,5.0,Three Rivers -E07000103,8.0,5.0,Watford -E07000105,12.0,-1.0,Ashford -E07000106,15.0,0.0,Canterbury -E07000107,13.0,1.0,Dartford -E07000108,14.0,-1.0,Dover -E07000109,14.0,1.0,Gravesham -E07000110,14.0,0.0,Maidstone -E07000111,12.0,0.0,Sevenoaks -E07000112,13.0,-1.0,Folkestone and Hythe -E07000113,16.0,0.0,Swale -E07000114,15.0,-1.0,Thanet -E07000115,13.0,0.0,Tonbridge and Malling -E07000116,11.0,-1.0,Tunbridge Wells -E07000117,6.0,15.0,Burnley -E07000118,3.0,14.0,Chorley -E07000119,4.0,16.0,Fylde -E07000120,5.0,15.0,Hyndburn -E07000121,3.0,17.0,Lancaster -E07000122,6.0,16.0,Pendle -E07000123,5.0,16.0,Preston -E07000124,5.0,17.0,Ribble Valley -E07000125,6.0,14.0,Rossendale -E07000126,3.0,15.0,South Ribble -E07000127,2.0,13.0,West Lancashire -E07000128,3.0,16.0,Wyre -E07000129,7.0,7.0,Blaby -E07000130,8.0,9.0,Charnwood -E07000131,8.0,7.0,Harborough -E07000132,7.0,8.0,Hinckley and Bosworth -E07000133,11.0,10.0,Melton -E07000134,6.0,9.0,North West Leicestershire -E07000135,9.0,8.0,Oadby and Wigston -E07000136,12.0,12.0,Boston -E07000137,12.0,13.0,East Lindsey -E07000138,11.0,12.0,Lincoln -E07000139,11.0,11.0,North Kesteven -E07000140,12.0,11.0,South Holland -E07000141,12.0,10.0,South Kesteven -E07000142,11.0,13.0,West Lindsey -E07000143,14.0,10.0,Breckland -E07000144,15.0,12.0,Broadland -E07000145,15.0,11.0,Great Yarmouth -E07000146,13.0,11.0,King's Lynn and West Norfolk -E07000147,14.0,12.0,North Norfolk -E07000148,14.0,11.0,Norwich -E07000149,15.0,10.0,South Norfolk -E07000170,8.0,11.0,Ashfield -E07000171,10.0,13.0,Bassetlaw -E07000172,7.0,10.0,Broxtowe -E07000173,9.0,10.0,Gedling -E07000174,9.0,11.0,Mansfield -E07000175,10.0,11.0,Newark and Sherwood -E07000176,10.0,10.0,Rushcliffe -E07000177,4.0,5.0,Cherwell -E07000178,4.0,4.0,Oxford -E07000179,5.0,4.0,South Oxfordshire -E07000180,3.0,4.0,Vale of White Horse -E07000181,3.0,5.0,West Oxfordshire -E07000192,3.0,9.0,Cannock Chase -E07000193,5.0,11.0,East Staffordshire -E07000194,4.0,9.0,Lichfield -E07000195,2.0,10.0,Newcastle-under-Lyme -E07000196,2.0,8.0,South Staffordshire -E07000197,4.0,10.0,Stafford -E07000198,5.0,10.0,Staffordshire Moorlands -E07000199,5.0,9.0,Tamworth -E07000200,14.0,8.0,Babergh -E07000202,15.0,9.0,Ipswich -E07000203,14.0,9.0,Mid Suffolk -E07000207,7.0,2.0,Elmbridge -E07000208,8.0,0.0,Epsom and Ewell -E07000209,5.0,1.0,Guildford -E07000210,6.0,1.0,Mole Valley -E07000211,7.0,0.0,Reigate and Banstead -E07000212,5.0,3.0,Runnymede -E07000213,6.0,3.0,Spelthorne -E07000214,5.0,2.0,Surrey Heath -E07000215,9.0,-1.0,Tandridge -E07000216,6.0,0.0,Waverley -E07000217,6.0,2.0,Woking -E07000218,6.0,8.0,North Warwickshire -E07000219,6.0,7.0,Nuneaton and Bedworth -E07000220,6.0,6.0,Rugby -E07000221,3.0,6.0,Stratford-on-Avon -E07000222,4.0,6.0,Warwick -E07000223,8.0,-2.0,Adur -E07000224,6.0,-2.0,Arun -E07000225,5.0,-1.0,Chichester -E07000226,8.0,-1.0,Crawley -E07000227,6.0,-1.0,Horsham -E07000228,7.0,-1.0,Mid Sussex -E07000229,7.0,-2.0,Worthing -E07000234,2.0,7.0,Bromsgrove -E07000235,-1.0,7.0,Malvern Hills -E07000236,4.0,7.0,Redditch -E07000237,0.0,7.0,Worcester -E07000238,2.0,6.0,Wychavon -E07000239,1.0,8.0,Wyre Forest -E07000240,10.0,6.0,St Albans -E07000241,11.0,6.0,Welwyn Hatfield -E07000242,13.0,8.0,East Hertfordshire -E07000243,12.0,7.0,Stevenage -E07000244,16.0,10.0,East Suffolk -E07000245,13.0,9.0,West Suffolk -E08000001,4.0,14.0,Bolton -E08000002,5.0,14.0,Bury -E08000003,5.0,12.0,Manchester -E08000004,5.0,13.0,Oldham -E08000005,7.0,14.0,Rochdale -E08000006,4.0,13.0,Salford -E08000007,6.0,12.0,Stockport -E08000008,6.0,13.0,Tameside -E08000009,4.0,12.0,Trafford -E08000010,3.0,13.0,Wigan -E08000011,2.0,12.0,Knowsley -E08000012,1.0,13.0,Liverpool -E08000013,3.0,12.0,St. Helens -E08000014,2.0,14.0,Sefton -E08000015,1.0,12.0,Wirral -E08000016,8.0,14.0,Barnsley -E08000017,9.0,14.0,Doncaster -E08000018,9.0,13.0,Rotherham -E08000019,8.0,13.0,Sheffield -E08000021,5.0,19.0,Newcastle upon Tyne -E08000022,6.0,20.0,North Tyneside -E08000023,7.0,20.0,South Tyneside -E08000024,7.0,19.0,Sunderland -E08000025,5.0,8.0,Birmingham -E08000026,5.0,6.0,Coventry -E08000027,1.0,7.0,Dudley -E08000028,3.0,7.0,Sandwell -E08000029,5.0,7.0,Solihull -E08000030,4.0,8.0,Walsall -E08000031,3.0,8.0,Wolverhampton -E08000032,7.0,16.0,Bradford -E08000033,7.0,15.0,Calderdale -E08000034,8.0,15.0,Kirklees -E08000035,8.0,16.0,Leeds -E08000036,9.0,15.0,Wakefield -E08000037,6.0,19.0,Gateshead -E09000001,11.0,2.0,City of London -E09000002,13.0,3.0,Barking and Dagenham -E09000003,10.0,5.0,Barnet -E09000004,12.0,1.0,Bexley -E09000005,10.0,4.0,Brent -E09000006,11.0,0.0,Bromley -E09000007,11.0,4.0,Camden -E09000008,10.0,0.0,Croydon -E09000009,9.0,4.0,Ealing -E09000010,11.0,5.0,Enfield -E09000011,11.0,1.0,Greenwich -E09000012,12.0,3.0,Hackney -E09000013,8.0,3.0,Hammersmith and Fulham -E09000014,12.0,4.0,Haringey -E09000015,8.0,4.0,Harrow -E09000016,14.0,3.0,Havering -E09000017,7.0,4.0,Hillingdon -E09000018,7.0,3.0,Hounslow -E09000019,11.0,3.0,Islington -E09000020,9.0,3.0,Kensington and Chelsea -E09000021,7.0,1.0,Kingston upon Thames -E09000022,10.0,2.0,Lambeth -E09000023,10.0,1.0,Lewisham -E09000024,8.0,1.0,Merton -E09000025,13.0,2.0,Newham -E09000026,14.0,4.0,Redbridge -E09000027,8.0,2.0,Richmond upon Thames -E09000028,9.0,1.0,Southwark -E09000029,9.0,0.0,Sutton -E09000030,12.0,2.0,Tower Hamlets -E09000031,13.0,4.0,Waltham Forest -E09000032,9.0,2.0,Wandsworth -E09000033,10.0,3.0,Westminster -N09000001,-4.0,16.0,Antrim and Newtownabbey -N09000002,-5.0,16.0,"Armagh City, Banbridge and Craigavon" -N09000003,-4.0,17.0,Belfast -N09000004,-5.0,18.0,Causeway Coast and Glens -N09000005,-6.0,17.0,Derry City and Strabane -N09000006,-6.0,16.0,Fermanagh and Omagh -N09000007,-5.0,15.0,Lisburn and Castlereagh -N09000008,-4.0,18.0,Mid and East Antrim -N09000009,-5.0,17.0,Mid Ulster -N09000010,-4.0,15.0,"Newry, Mourne and Down" -S12000005,2.0,24.0,Clackmannanshire -S12000006,4.0,20.0,Dumfries and Galloway -S12000008,3.0,20.0,East Ayrshire -S12000010,5.0,22.0,East Lothian -S12000011,2.0,20.0,East Renfrewshire -S12000013,-1.0,27.0,Na h-Eileanan Siar -S12000014,2.0,23.0,Falkirk -S12000017,1.0,26.0,Highland -S12000018,0.0,21.0,Inverclyde -S12000019,3.0,21.0,Midlothian -S12000020,2.0,26.0,Moray -S12000021,1.0,20.0,North Ayrshire -S12000023,4.0,28.0,Orkney Islands -S12000026,4.0,21.0,Scottish Borders -S12000027,5.0,30.0,Shetland Islands -S12000028,1.0,19.0,South Ayrshire -S12000029,2.0,21.0,South Lanarkshire -S12000030,1.0,24.0,Stirling -S12000033,4.0,26.0,Aberdeen City -S12000034,3.0,26.0,Aberdeenshire -S12000035,0.0,24.0,Argyll and Bute -S12000036,4.0,22.0,City of Edinburgh -S12000038,1.0,22.0,Renfrewshire -S12000039,0.0,23.0,West Dunbartonshire -S12000040,3.0,22.0,West Lothian -S12000041,2.0,25.0,Angus -S12000042,3.0,25.0,Dundee City -S12000045,1.0,23.0,East Dunbartonshire -S12000047,3.0,24.0,Fife -S12000048,1.0,25.0,Perth and Kinross -S12000049,1.0,21.0,Glasgow City -S12000050,2.0,22.0,North Lanarkshire -W06000001,-2.0,12.0,Isle of Anglesey -W06000002,-2.0,10.0,Gwynedd -W06000003,-1.0,10.0,Conwy -W06000004,0.0,10.0,Denbighshire -W06000005,0.0,11.0,Flintshire -W06000006,1.0,10.0,Wrexham -W06000008,-2.0,9.0,Ceredigion -W06000009,-5.0,6.0,Pembrokeshire -W06000010,-4.0,6.0,Carmarthenshire -W06000011,-4.0,5.0,Swansea -W06000012,-3.0,5.0,Neath Port Talbot -W06000013,-3.0,6.0,Bridgend -W06000014,-2.0,4.0,Vale of Glamorgan -W06000015,-2.0,5.0,Cardiff -W06000016,-3.0,7.0,Rhondda Cynon Taf -W06000018,-2.0,6.0,Caerphilly -W06000019,0.0,9.0,Blaenau Gwent -W06000020,-2.0,7.0,Torfaen -W06000021,-1.0,8.0,Monmouthshire -W06000022,-1.0,5.0,Newport -W06000023,-1.0,9.0,Powys -W06000024,-2.0,8.0,Merthyr Tydfil diff --git a/app/src/data/static/regions/uk/localAuthorities.ts b/app/src/data/static/regions/uk/localAuthorities.ts deleted file mode 100644 index 97f5738b3..000000000 --- a/app/src/data/static/regions/uk/localAuthorities.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * UK Local Authorities (2021 Boundaries) - * - * ~360 local authorities total - * - * Data source: policyengine-api/data/local_authorities_2021.csv - * Source: Office for National Statistics - */ - -import { MetadataRegionEntry } from '@/types/metadata'; -import { UK_REGION_TYPES } from '@/types/regionTypes'; -import { RegionVersionMeta, VersionedRegionSet } from '../types'; -import localAuthoritiesCSV from './data/local_authorities_2021.csv?raw'; - -/** - * Parse CSV data into local authority entries - * CSV format: code,x,y,name - */ -function parseLocalAuthorities(csv: string): MetadataRegionEntry[] { - const lines = csv.trim().split('\n').slice(1); // Skip header - const authorities: MetadataRegionEntry[] = []; - - for (const line of lines) { - if (!line.trim()) { - continue; - } - - // Handle CSV with potential quoted fields containing commas - let name: string; - const parts = line.split(','); - - if (line.includes('"')) { - // Find the quoted name (it's the last field in this CSV) - const quoteStart = line.indexOf('"'); - const quoteEnd = line.lastIndexOf('"'); - name = line.substring(quoteStart + 1, quoteEnd); - } else { - // Simple case: code,x,y,name - name = parts.slice(3).join(','); // Name might have commas - } - - authorities.push({ - name: `local_authority/${name}`, - label: name, - type: UK_REGION_TYPES.LOCAL_AUTHORITY, - }); - } - - return authorities.sort((a, b) => a.label.localeCompare(b.label)); -} - -const VERSION_2021: RegionVersionMeta = { - version: '2021', - effectiveFrom: 2021, - effectiveUntil: null, - description: 'Local authority boundaries as of 2021', - source: 'https://www.ons.gov.uk/', -}; - -// Parse local authorities once at module load -const LOCAL_AUTHORITIES_2021 = parseLocalAuthorities(localAuthoritiesCSV); - -export const UK_LOCAL_AUTHORITIES: VersionedRegionSet = { - versions: { - '2021': { - meta: VERSION_2021, - data: LOCAL_AUTHORITIES_2021, - }, - }, - getVersionForYear: (_year: number): string => { - // 2021 boundaries are currently the only version - return '2021'; - }, -}; diff --git a/app/src/data/static/regions/us/congressionalDistricts.ts b/app/src/data/static/regions/us/congressionalDistricts.ts deleted file mode 100644 index b33a58444..000000000 --- a/app/src/data/static/regions/us/congressionalDistricts.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * US Congressional Districts (2020 Census Apportionment) - * - * 436 districts total (435 voting + DC non-voting delegate) - * Effective for 118th Congress (2023-2025) through at least 119th Congress (2025-2027) - * - * Source: https://ballotpedia.org/Congressional_apportionment_after_the_2020_census - */ - -import { MetadataRegionEntry } from '@/types/metadata'; -import { US_REGION_TYPES } from '@/types/regionTypes'; -import { RegionVersionMeta, VersionedRegionSet } from '../types'; - -// State code to full name mapping -const STATE_NAMES: Record = { - AL: 'Alabama', - AK: 'Alaska', - AZ: 'Arizona', - AR: 'Arkansas', - CA: 'California', - CO: 'Colorado', - CT: 'Connecticut', - DE: 'Delaware', - DC: 'District of Columbia', - FL: 'Florida', - GA: 'Georgia', - HI: 'Hawaii', - ID: 'Idaho', - IL: 'Illinois', - IN: 'Indiana', - IA: 'Iowa', - KS: 'Kansas', - KY: 'Kentucky', - LA: 'Louisiana', - ME: 'Maine', - MD: 'Maryland', - MA: 'Massachusetts', - MI: 'Michigan', - MN: 'Minnesota', - MS: 'Mississippi', - MO: 'Missouri', - MT: 'Montana', - NE: 'Nebraska', - NV: 'Nevada', - NH: 'New Hampshire', - NJ: 'New Jersey', - NM: 'New Mexico', - NY: 'New York', - NC: 'North Carolina', - ND: 'North Dakota', - OH: 'Ohio', - OK: 'Oklahoma', - OR: 'Oregon', - PA: 'Pennsylvania', - RI: 'Rhode Island', - SC: 'South Carolina', - SD: 'South Dakota', - TN: 'Tennessee', - TX: 'Texas', - UT: 'Utah', - VT: 'Vermont', - VA: 'Virginia', - WA: 'Washington', - WV: 'West Virginia', - WI: 'Wisconsin', - WY: 'Wyoming', -}; - -// States with only one at-large district -const AT_LARGE_STATES = new Set(['AK', 'DE', 'DC', 'ND', 'SD', 'VT', 'WY']); - -// District counts by state (2020 Census apportionment) -// Format: [stateCode, districtCount] -const DISTRICT_COUNTS_2020: [string, number][] = [ - ['AL', 7], - ['AK', 1], - ['AZ', 9], - ['AR', 4], - ['CA', 52], - ['CO', 8], - ['CT', 5], - ['DE', 1], - ['DC', 1], - ['FL', 28], - ['GA', 14], - ['HI', 2], - ['ID', 2], - ['IL', 17], - ['IN', 9], - ['IA', 4], - ['KS', 4], - ['KY', 6], - ['LA', 6], - ['ME', 2], - ['MD', 8], - ['MA', 9], - ['MI', 13], - ['MN', 8], - ['MS', 4], - ['MO', 8], - ['MT', 2], - ['NE', 3], - ['NV', 4], - ['NH', 2], - ['NJ', 12], - ['NM', 3], - ['NY', 26], - ['NC', 14], - ['ND', 1], - ['OH', 15], - ['OK', 5], - ['OR', 6], - ['PA', 17], - ['RI', 2], - ['SC', 7], - ['SD', 1], - ['TN', 9], - ['TX', 38], - ['UT', 4], - ['VT', 1], - ['VA', 11], - ['WA', 10], - ['WV', 2], - ['WI', 8], - ['WY', 1], -]; - -/** - * Get ordinal suffix for a number (1st, 2nd, 3rd, 4th, etc.) - */ -function getOrdinalSuffix(n: number): string { - if (n % 100 >= 11 && n % 100 <= 13) { - return 'th'; - } - switch (n % 10) { - case 1: - return 'st'; - case 2: - return 'nd'; - case 3: - return 'rd'; - default: - return 'th'; - } -} - -/** - * Build district label (e.g., "California's 1st congressional district") - */ -function buildDistrictLabel(stateCode: string, districtNumber: number): string { - const stateName = STATE_NAMES[stateCode]; - if (AT_LARGE_STATES.has(stateCode)) { - return `${stateName}'s at-large congressional district`; - } - return `${stateName}'s ${districtNumber}${getOrdinalSuffix(districtNumber)} congressional district`; -} - -/** - * Generate all congressional district entries from compact data - */ -function buildCongressionalDistricts(districtCounts: [string, number][]): MetadataRegionEntry[] { - const districts: MetadataRegionEntry[] = []; - - for (const [stateCode, count] of districtCounts) { - for (let i = 1; i <= count; i++) { - const districtNum = i.toString().padStart(2, '0'); - districts.push({ - name: `congressional_district/${stateCode}-${districtNum}`, - label: buildDistrictLabel(stateCode, i), - type: US_REGION_TYPES.CONGRESSIONAL_DISTRICT, - state_abbreviation: stateCode, - state_name: STATE_NAMES[stateCode], - }); - } - } - - return districts; -} - -const VERSION_2020_CENSUS: RegionVersionMeta = { - version: '2020-census', - effectiveFrom: 2023, - effectiveUntil: null, - description: 'Districts based on 2020 Census apportionment (118th-119th Congress)', - source: 'https://ballotpedia.org/Congressional_apportionment_after_the_2020_census', -}; - -// Generate districts once at module load -const DISTRICTS_2020_CENSUS = buildCongressionalDistricts(DISTRICT_COUNTS_2020); - -export const US_CONGRESSIONAL_DISTRICTS: VersionedRegionSet = { - versions: { - '2020-census': { - meta: VERSION_2020_CENSUS, - data: DISTRICTS_2020_CENSUS, - }, - }, - getVersionForYear: (_year: number): string => { - // 2020 Census districts effective from 2023 onwards - // For years before 2023, still return 2020-census as fallback - return '2020-census'; - }, -}; diff --git a/app/src/data/static/staticRegions.ts b/app/src/data/static/staticRegions.ts deleted file mode 100644 index a53a227be..000000000 --- a/app/src/data/static/staticRegions.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Static region definitions for US and UK - * These define the geographic regions available for economy-wide simulations - */ - -import { MetadataRegionEntry } from '@/types/metadata'; -import { UK_REGION_TYPES, US_REGION_TYPES } from '@/types/regionTypes'; - -/** - * US region options - * Note: Congressional districts are dynamically loaded, not included here - */ -export const US_REGIONS: MetadataRegionEntry[] = [ - { name: 'us', label: 'United States', type: US_REGION_TYPES.NATIONAL }, - { name: 'state/al', label: 'Alabama', type: US_REGION_TYPES.STATE }, - { name: 'state/ak', label: 'Alaska', type: US_REGION_TYPES.STATE }, - { name: 'state/az', label: 'Arizona', type: US_REGION_TYPES.STATE }, - { name: 'state/ar', label: 'Arkansas', type: US_REGION_TYPES.STATE }, - { name: 'state/ca', label: 'California', type: US_REGION_TYPES.STATE }, - { name: 'state/co', label: 'Colorado', type: US_REGION_TYPES.STATE }, - { name: 'state/ct', label: 'Connecticut', type: US_REGION_TYPES.STATE }, - { name: 'state/de', label: 'Delaware', type: US_REGION_TYPES.STATE }, - { name: 'state/fl', label: 'Florida', type: US_REGION_TYPES.STATE }, - { name: 'state/ga', label: 'Georgia', type: US_REGION_TYPES.STATE }, - { name: 'state/hi', label: 'Hawaii', type: US_REGION_TYPES.STATE }, - { name: 'state/id', label: 'Idaho', type: US_REGION_TYPES.STATE }, - { name: 'state/il', label: 'Illinois', type: US_REGION_TYPES.STATE }, - { name: 'state/in', label: 'Indiana', type: US_REGION_TYPES.STATE }, - { name: 'state/ia', label: 'Iowa', type: US_REGION_TYPES.STATE }, - { name: 'state/ks', label: 'Kansas', type: US_REGION_TYPES.STATE }, - { name: 'state/ky', label: 'Kentucky', type: US_REGION_TYPES.STATE }, - { name: 'state/la', label: 'Louisiana', type: US_REGION_TYPES.STATE }, - { name: 'state/me', label: 'Maine', type: US_REGION_TYPES.STATE }, - { name: 'state/md', label: 'Maryland', type: US_REGION_TYPES.STATE }, - { name: 'state/ma', label: 'Massachusetts', type: US_REGION_TYPES.STATE }, - { name: 'state/mi', label: 'Michigan', type: US_REGION_TYPES.STATE }, - { name: 'state/mn', label: 'Minnesota', type: US_REGION_TYPES.STATE }, - { name: 'state/ms', label: 'Mississippi', type: US_REGION_TYPES.STATE }, - { name: 'state/mo', label: 'Missouri', type: US_REGION_TYPES.STATE }, - { name: 'state/mt', label: 'Montana', type: US_REGION_TYPES.STATE }, - { name: 'state/ne', label: 'Nebraska', type: US_REGION_TYPES.STATE }, - { name: 'state/nv', label: 'Nevada', type: US_REGION_TYPES.STATE }, - { name: 'state/nh', label: 'New Hampshire', type: US_REGION_TYPES.STATE }, - { name: 'state/nj', label: 'New Jersey', type: US_REGION_TYPES.STATE }, - { name: 'state/nm', label: 'New Mexico', type: US_REGION_TYPES.STATE }, - { name: 'state/ny', label: 'New York', type: US_REGION_TYPES.STATE }, - { name: 'state/nc', label: 'North Carolina', type: US_REGION_TYPES.STATE }, - { name: 'state/nd', label: 'North Dakota', type: US_REGION_TYPES.STATE }, - { name: 'state/oh', label: 'Ohio', type: US_REGION_TYPES.STATE }, - { name: 'state/ok', label: 'Oklahoma', type: US_REGION_TYPES.STATE }, - { name: 'state/or', label: 'Oregon', type: US_REGION_TYPES.STATE }, - { name: 'state/pa', label: 'Pennsylvania', type: US_REGION_TYPES.STATE }, - { name: 'state/ri', label: 'Rhode Island', type: US_REGION_TYPES.STATE }, - { name: 'state/sc', label: 'South Carolina', type: US_REGION_TYPES.STATE }, - { name: 'state/sd', label: 'South Dakota', type: US_REGION_TYPES.STATE }, - { name: 'state/tn', label: 'Tennessee', type: US_REGION_TYPES.STATE }, - { name: 'state/tx', label: 'Texas', type: US_REGION_TYPES.STATE }, - { name: 'state/ut', label: 'Utah', type: US_REGION_TYPES.STATE }, - { name: 'state/vt', label: 'Vermont', type: US_REGION_TYPES.STATE }, - { name: 'state/va', label: 'Virginia', type: US_REGION_TYPES.STATE }, - { name: 'state/wa', label: 'Washington', type: US_REGION_TYPES.STATE }, - { name: 'state/wv', label: 'West Virginia', type: US_REGION_TYPES.STATE }, - { name: 'state/wi', label: 'Wisconsin', type: US_REGION_TYPES.STATE }, - { name: 'state/wy', label: 'Wyoming', type: US_REGION_TYPES.STATE }, - { name: 'state/dc', label: 'District of Columbia', type: US_REGION_TYPES.STATE }, - { name: 'city/nyc', label: 'New York City', type: US_REGION_TYPES.CITY }, -]; - -/** - * UK region options - * Note: Constituencies are dynamically loaded, not included here - */ -export const UK_REGIONS: MetadataRegionEntry[] = [ - { name: 'uk', label: 'United Kingdom', type: UK_REGION_TYPES.NATIONAL }, - { name: 'country/england', label: 'England', type: UK_REGION_TYPES.COUNTRY }, - { name: 'country/scotland', label: 'Scotland', type: UK_REGION_TYPES.COUNTRY }, - { name: 'country/wales', label: 'Wales', type: UK_REGION_TYPES.COUNTRY }, - { name: 'country/northern_ireland', label: 'Northern Ireland', type: UK_REGION_TYPES.COUNTRY }, -]; - -/** - * Get regions for a country - */ -export function getRegions(countryId: string): MetadataRegionEntry[] { - switch (countryId) { - case 'us': - return US_REGIONS; - case 'uk': - return UK_REGIONS; - default: - return []; - } -} diff --git a/app/src/hooks/useStaticMetadata.ts b/app/src/hooks/useStaticMetadata.ts index 384e80acb..46e828844 100644 --- a/app/src/hooks/useStaticMetadata.ts +++ b/app/src/hooks/useStaticMetadata.ts @@ -7,15 +7,17 @@ * Usage: * ```typescript * // Get everything - * const { entities, basicInputs, timePeriods, regions, modelledPolicies, currentLawId } = - * useStaticMetadata('us', 2025); + * const { entities, basicInputs, timePeriods, modelledPolicies, currentLawId } = + * useStaticMetadata('us'); * * // Or destructure just what you need - * const { entities, basicInputs } = useStaticMetadata('us', 2025); + * const { entities, basicInputs } = useStaticMetadata('us'); * * // Individual hooks are also available * const entities = useEntities('us'); - * const { regions, versions } = useRegions('us', 2025); + * + * // For regions, use the V2 API hook: + * const { regions, isLoading } = useRegions('us'); * ``` */ @@ -30,11 +32,11 @@ import { type ModelledPolicies, type TimePeriodOption, } from '@/data/static'; -import { resolveRegions, type ResolvedRegions } from '@/data/static/regions'; -import { MetadataRegionEntry } from '@/types/metadata'; /** * All static metadata for a country + * + * Note: Regions are now fetched from the V2 API via useRegions() hook. */ export interface StaticMetadata { /** Entity definitions (person, family, household, etc.) */ @@ -43,10 +45,6 @@ export interface StaticMetadata { basicInputs: string[]; /** Available simulation years */ timePeriods: TimePeriodOption[]; - /** Geographic regions (states, districts, constituencies, etc.) */ - regions: MetadataRegionEntry[]; - /** Region version info (which boundary set is active) */ - regionVersions: ResolvedRegions['versions']; /** Pre-configured policy options */ modelledPolicies: ModelledPolicies; /** ID of the current law baseline policy */ @@ -54,28 +52,26 @@ export interface StaticMetadata { } /** - * Get all static metadata for a country and simulation year + * Get all static metadata for a country * * This is the primary hook for accessing static metadata. It bundles * all static data into a single object for easy destructuring. * + * Note: Regions are not included here - use useRegions() from @/hooks/useRegions + * to fetch region data from the V2 API. + * * @param countryId - Country code ('us' or 'uk') - * @param year - Simulation year (affects which region boundaries are used) */ -export function useStaticMetadata(countryId: string, year: number): StaticMetadata { +export function useStaticMetadata(countryId: string): StaticMetadata { return useMemo(() => { - const { regions, versions } = resolveRegions(countryId, year); - return { entities: getEntities(countryId), basicInputs: getBasicInputs(countryId), timePeriods: getTimePeriods(countryId), - regions, - regionVersions: versions, modelledPolicies: getModelledPolicies(countryId), currentLawId: getCurrentLawId(countryId), }; - }, [countryId, year]); + }, [countryId]); } // ============================================================================ diff --git a/app/src/tests/unit/hooks/useSaveSharedReport.test.tsx b/app/src/tests/unit/hooks/useSaveSharedReport.test.tsx index c273f3df0..1c5085f53 100644 --- a/app/src/tests/unit/hooks/useSaveSharedReport.test.tsx +++ b/app/src/tests/unit/hooks/useSaveSharedReport.test.tsx @@ -22,7 +22,6 @@ import { const mockCreateSimulation = createMockMutation(); const mockCreatePolicy = createMockMutation(); const mockCreateHousehold = createMockMutation(); -const mockCreateGeography = createMockMutation(); const mockCreateReport = createMockMutation(); const mockReportStore = createMockReportStore(); @@ -38,10 +37,6 @@ vi.mock('@/hooks/useUserHousehold', () => ({ useCreateHouseholdAssociation: () => mockCreateHousehold, })); -vi.mock('@/hooks/useUserGeographic', () => ({ - useCreateGeographicAssociation: () => mockCreateGeography, -})); - vi.mock('@/hooks/useUserReportAssociations', () => ({ useCreateReportAssociation: () => mockCreateReport, useUserReportStore: () => mockReportStore, @@ -75,7 +70,6 @@ describe('useSaveSharedReport', () => { mockCreateSimulation.mutateAsync.mockResolvedValue({}); mockCreatePolicy.mutateAsync.mockResolvedValue({}); mockCreateHousehold.mutateAsync.mockResolvedValue({}); - mockCreateGeography.mutateAsync.mockResolvedValue({}); mockCreateReport.mutateAsync.mockResolvedValue(MOCK_SAVED_USER_REPORT); mockReportStore.findByUserReportId.mockResolvedValue(null); }); @@ -106,13 +100,7 @@ describe('useSaveSharedReport', () => { countryId: 'us', label: 'My Policy', }); - expect(mockCreateGeography.mutateAsync).toHaveBeenCalledWith({ - userId: 'anonymous', - geographyId: 'us', - countryId: 'us', - scope: 'national', - label: 'United States', - }); + // Note: Geographies are no longer saved as user associations (constructed from simulation data) expect(mockCreateReport.mutateAsync).toHaveBeenCalled(); }); @@ -178,7 +166,7 @@ describe('useSaveSharedReport', () => { countryId: 'uk', label: 'My Household', }); - expect(mockCreateGeography.mutateAsync).not.toHaveBeenCalled(); + // Note: Geographies are no longer saved as user associations }); test('given shareData without label then generates default label', async () => { diff --git a/app/src/tests/unit/hooks/useSharedReportData.test.tsx b/app/src/tests/unit/hooks/useSharedReportData.test.tsx index e533f951b..3649923e7 100644 --- a/app/src/tests/unit/hooks/useSharedReportData.test.tsx +++ b/app/src/tests/unit/hooks/useSharedReportData.test.tsx @@ -152,12 +152,11 @@ describe('useSharedReportData', () => { expect(result.current.isLoading).toBe(false); }); - // Geography is constructed from simulation data + // Geography is constructed from simulation data using simplified format expect(result.current.geographies).toHaveLength(1); expect(result.current.geographies[0]).toMatchObject({ - id: 'us', countryId: 'us', - scope: 'national', + regionCode: 'us', }); // Note: userGeographies no longer returned - geographies are not user associations }); diff --git a/app/src/tests/unit/hooks/useStaticMetadata.test.ts b/app/src/tests/unit/hooks/useStaticMetadata.test.ts index 6a14c7ccf..94e5f6ff5 100644 --- a/app/src/tests/unit/hooks/useStaticMetadata.test.ts +++ b/app/src/tests/unit/hooks/useStaticMetadata.test.ts @@ -14,21 +14,20 @@ describe('useStaticMetadata', () => { describe('useStaticMetadata (composite hook)', () => { it('given US country then returns complete static metadata', () => { // When - const { result } = renderHook(() => useStaticMetadata(TEST_COUNTRIES.US, TEST_YEAR)); + const { result } = renderHook(() => useStaticMetadata(TEST_COUNTRIES.US)); // Then expect(result.current).toHaveProperty('entities'); expect(result.current).toHaveProperty('basicInputs'); expect(result.current).toHaveProperty('timePeriods'); - expect(result.current).toHaveProperty('regions'); - expect(result.current).toHaveProperty('regionVersions'); expect(result.current).toHaveProperty('modelledPolicies'); expect(result.current).toHaveProperty('currentLawId'); + // Note: regions are now fetched from V2 API via useRegions() hook }); it('given US country then entities contains person entity', () => { // When - const { result } = renderHook(() => useStaticMetadata(TEST_COUNTRIES.US, TEST_YEAR)); + const { result } = renderHook(() => useStaticMetadata(TEST_COUNTRIES.US)); // Then expect(result.current.entities).toHaveProperty('person'); @@ -37,7 +36,7 @@ describe('useStaticMetadata', () => { it('given US country then basicInputs includes age and employment_income', () => { // When - const { result } = renderHook(() => useStaticMetadata(TEST_COUNTRIES.US, TEST_YEAR)); + const { result } = renderHook(() => useStaticMetadata(TEST_COUNTRIES.US)); // Then expect(result.current.basicInputs).toContain('age'); @@ -46,28 +45,13 @@ describe('useStaticMetadata', () => { it('given UK country then returns UK-specific entities', () => { // When - const { result } = renderHook(() => useStaticMetadata(TEST_COUNTRIES.UK, TEST_YEAR)); + const { result } = renderHook(() => useStaticMetadata(TEST_COUNTRIES.UK)); // Then expect(result.current.entities).toHaveProperty('person'); expect(result.current.entities).toHaveProperty('benunit'); expect(result.current.entities).not.toHaveProperty('tax_unit'); }); - - it('given year change then updates regions', () => { - // Given - const { result, rerender } = renderHook( - ({ year }) => useStaticMetadata(TEST_COUNTRIES.US, year), - { initialProps: { year: 2024 } } - ); - const firstRegions = result.current.regions; - - // When - rerender({ year: 2025 }); - - // Then - regions array reference should be stable if content is same - expect(result.current.regions).toBeDefined(); - }); }); describe('useEntities', () => { diff --git a/app/src/tests/unit/utils/regionStrategies.test.ts b/app/src/tests/unit/utils/regionStrategies.test.ts index fa2d0f983..a1389936b 100644 --- a/app/src/tests/unit/utils/regionStrategies.test.ts +++ b/app/src/tests/unit/utils/regionStrategies.test.ts @@ -413,10 +413,8 @@ describe('regionStrategies', () => { // Then expect(result).toEqual({ - id: 'uk', countryId: 'uk', - scope: 'national', - geographyId: 'uk', + regionCode: 'uk', }); }); @@ -430,10 +428,8 @@ describe('regionStrategies', () => { // Then expect(result).toEqual({ - id: 'us', countryId: 'us', - scope: 'national', - geographyId: 'us', + regionCode: 'us', }); }); @@ -448,10 +444,8 @@ describe('regionStrategies', () => { // Then expect(result).toEqual({ - id: 'uk-Sheffield Central', // ID uses display value countryId: 'uk', - scope: 'subnational', - geographyId: TEST_REGIONS.UK_CONSTITUENCY_PREFIXED, // Stores FULL prefixed value + regionCode: TEST_REGIONS.UK_CONSTITUENCY_PREFIXED, // Stores FULL prefixed value }); }); @@ -466,10 +460,8 @@ describe('regionStrategies', () => { // Then expect(result).toEqual({ - id: 'uk-england', // ID uses display value countryId: 'uk', - scope: 'subnational', - geographyId: TEST_REGIONS.UK_COUNTRY_PREFIXED, // Stores FULL prefixed value + regionCode: TEST_REGIONS.UK_COUNTRY_PREFIXED, // Stores FULL prefixed value }); }); @@ -484,10 +476,8 @@ describe('regionStrategies', () => { // Then expect(result).toEqual({ - id: 'uk-Maidstone', // ID uses display value countryId: 'uk', - scope: 'subnational', - geographyId: TEST_REGIONS.UK_LOCAL_AUTHORITY_PREFIXED, // Stores FULL prefixed value + regionCode: TEST_REGIONS.UK_LOCAL_AUTHORITY_PREFIXED, // Stores FULL prefixed value }); }); @@ -502,10 +492,8 @@ describe('regionStrategies', () => { // Then expect(result).toEqual({ - id: 'us-ca', // ID uses display value countryId: 'us', - scope: 'subnational', - geographyId: TEST_REGIONS.US_STATE, // Stores FULL prefixed value + regionCode: TEST_REGIONS.US_STATE, // Stores FULL prefixed value }); }); @@ -520,10 +508,8 @@ describe('regionStrategies', () => { // Then expect(result).toEqual({ - id: 'us-CA-01', // ID uses display value countryId: 'us', - scope: 'subnational', - geographyId: TEST_REGIONS.US_CONGRESSIONAL_DISTRICT, // Stores FULL prefixed value + regionCode: TEST_REGIONS.US_CONGRESSIONAL_DISTRICT, // Stores FULL prefixed value }); }); @@ -574,10 +560,8 @@ describe('regionStrategies', () => { // Then - Should still work with legacy format expect(result).toEqual({ - id: 'us-tx', countryId: 'us', - scope: 'subnational', - geographyId: 'tx', // Legacy format preserved + regionCode: 'tx', // Legacy format preserved }); }); @@ -592,10 +576,8 @@ describe('regionStrategies', () => { // Then expect(result).toEqual({ - id: 'us-ca', countryId: 'us', - scope: 'subnational', - geographyId: 'ca', // Legacy format preserved + regionCode: 'ca', // Legacy format preserved }); }); }); From 9a5f7ff14e914bfd0fb37ce2bd1fc97474e20f24 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Fri, 13 Feb 2026 22:54:44 +0100 Subject: [PATCH 07/17] feat: Use API v2 label system --- app/src/pages/Simulations.page.tsx | 9 ++- .../pages/report-output/GeographySubPage.tsx | 57 ++++++++++++------- .../pathways/report/views/ReportSetupView.tsx | 47 +++++++++++---- .../views/ReportSimulationExistingView.tsx | 16 +++++- .../views/simulation/SimulationSetupView.tsx | 22 ++++++- .../unit/pages/Simulations.page.test.tsx | 10 ++++ .../report-output/GeographySubPage.test.tsx | 35 +++++++++--- .../ReportSimulationExistingView.test.tsx | 14 +++++ .../simulation/SimulationSetupView.test.tsx | 14 +++++ app/src/utils/geographyUtils.ts | 3 +- 10 files changed, 184 insertions(+), 43 deletions(-) diff --git a/app/src/pages/Simulations.page.tsx b/app/src/pages/Simulations.page.tsx index 182758c36..cdbe459fb 100644 --- a/app/src/pages/Simulations.page.tsx +++ b/app/src/pages/Simulations.page.tsx @@ -7,15 +7,18 @@ import { RenameIngredientModal } from '@/components/common/RenameIngredientModal import IngredientReadView from '@/components/IngredientReadView'; import { MOCK_USER_ID } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useRegions } from '@/hooks/useRegions'; import { useUpdateSimulationAssociation } from '@/hooks/useUserSimulationAssociations'; import { useUserSimulations } from '@/hooks/useUserSimulations'; import { formatDate } from '@/utils/dateUtils'; +import { getRegionLabel, isNationalGeography, getCountryLabel } from '@/utils/geographyUtils'; export default function SimulationsPage() { const userId = MOCK_USER_ID.toString(); // TODO: Replace with actual user ID retrieval logic const { data, isLoading, isError, error } = useUserSimulations(userId); const navigate = useNavigate(); const countryId = useCurrentCountry(); + const { regions } = useRegions(countryId); const [searchValue, setSearchValue] = useState(''); const [selectedIds, setSelectedIds] = useState([]); @@ -129,7 +132,11 @@ export default function SimulationsPage() { population: { text: item.userHousehold?.label || - item.geography?.regionCode || + (item.geography + ? isNationalGeography(item.geography) + ? getCountryLabel(item.geography.countryId) + : getRegionLabel(item.geography.regionCode, regions) + : null) || (item.household ? `Household #${item.household.id}` : 'No population'), } as TextValue, })) || []; diff --git a/app/src/pages/report-output/GeographySubPage.tsx b/app/src/pages/report-output/GeographySubPage.tsx index 59b4cafc4..53bd0682f 100644 --- a/app/src/pages/report-output/GeographySubPage.tsx +++ b/app/src/pages/report-output/GeographySubPage.tsx @@ -1,6 +1,8 @@ import { Box, Table, Text } from '@mantine/core'; import { colors, spacing, typography } from '@/designTokens'; +import { useRegions } from '@/hooks/useRegions'; import { Geography, isNationalGeography } from '@/types/ingredients/Geography'; +import { getCountryLabel, getRegionLabel, getRegionTypeLabel } from '@/utils/geographyUtils'; interface GeographySubPageProps { baselineGeography?: Geography; @@ -8,11 +10,24 @@ interface GeographySubPageProps { } /** - * Get display scope (national/subnational) from geography + * Get display scope label from geography using V2 API metadata */ -function getGeographyScope(geography: Geography | undefined): string { - if (!geography) return '—'; - return isNationalGeography(geography) ? 'National' : 'Subnational'; +function useGeographyDisplayInfo(geography: Geography | undefined, regions: ReturnType['regions']) { + if (!geography) { + return { label: '—', scopeLabel: '—' }; + } + + if (isNationalGeography(geography)) { + return { + label: getCountryLabel(geography.countryId), + scopeLabel: 'National', + }; + } + + return { + label: getRegionLabel(geography.regionCode, regions), + scopeLabel: getRegionTypeLabel(geography.countryId, geography.regionCode, regions), + }; } /** @@ -20,36 +35,40 @@ function getGeographyScope(geography: Geography | undefined): string { * * Shows baseline and reform geographies side-by-side in a comparison table. * Collapses columns when both simulations use the same geography. - * - * TODO (Phase 6.2): Look up display labels from region metadata using regionCode - * Currently displays regionCode directly as a fallback. + * Uses V2 API metadata to display human-readable region labels. */ export default function GeographySubPage({ baselineGeography, reformGeography, }: GeographySubPageProps) { + // Get country ID from either geography (they should be the same country) + const countryId = baselineGeography?.countryId || reformGeography?.countryId || 'us'; + + // Fetch regions from V2 API + const { regions, isLoading } = useRegions(countryId); + if (!baselineGeography && !reformGeography) { return
No geography data available
; } + // Get display info for both geographies + const baselineInfo = useGeographyDisplayInfo(baselineGeography, regions); + const reformInfo = useGeographyDisplayInfo(reformGeography, regions); + // Check if geographies are the same by comparing regionCode const geographiesAreSame = baselineGeography?.regionCode === reformGeography?.regionCode; - // Get labels - TODO (Phase 6.2): look up from region metadata - const baselineLabel = baselineGeography?.regionCode || 'Baseline'; - const reformLabel = reformGeography?.regionCode || 'Reform'; - - // Define table rows + // Define table rows using labels from V2 API const rows = [ { label: 'Geographic area', - baselineValue: baselineGeography?.regionCode || '—', - reformValue: reformGeography?.regionCode || '—', + baselineValue: isLoading ? '...' : baselineInfo.label, + reformValue: isLoading ? '...' : reformInfo.label, }, { label: 'Type', - baselineValue: getGeographyScope(baselineGeography), - reformValue: getGeographyScope(reformGeography), + baselineValue: isLoading ? '...' : baselineInfo.scopeLabel, + reformValue: isLoading ? '...' : reformInfo.scopeLabel, }, ]; @@ -99,7 +118,7 @@ export default function GeographySubPage({ padding: `${spacing.md} ${spacing.lg}`, }} > - {baselineLabel.toUpperCase()} (BASELINE / REFORM) + {baselineInfo.label.toUpperCase()} (BASELINE / REFORM) ) : ( <> @@ -115,7 +134,7 @@ export default function GeographySubPage({ padding: `${spacing.md} ${spacing.lg}`, }} > - {baselineLabel.toUpperCase()} (BASELINE) + {baselineInfo.label.toUpperCase()} (BASELINE) - {reformLabel.toUpperCase()} (REFORM) + {reformInfo.label.toUpperCase()} (REFORM) )} diff --git a/app/src/pathways/report/views/ReportSetupView.tsx b/app/src/pathways/report/views/ReportSetupView.tsx index e9a6adbb8..93ce5ee38 100644 --- a/app/src/pathways/report/views/ReportSetupView.tsx +++ b/app/src/pathways/report/views/ReportSetupView.tsx @@ -1,8 +1,12 @@ import { useState } from 'react'; import PathwayView from '@/components/common/PathwayView'; import { MOCK_USER_ID } from '@/constants'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useRegions } from '@/hooks/useRegions'; import { useUserHouseholds } from '@/hooks/useUserHousehold'; import { ReportStateProps, SimulationStateProps } from '@/types/pathwayState'; +import { MetadataRegionEntry } from '@/types/metadata'; +import { getCountryLabel, getRegionLabel, isNationalGeography } from '@/utils/geographyUtils'; import { isSimulationConfigured } from '@/utils/validation/ingredientValidation'; type SimulationCard = 'simulation1' | 'simulation2'; @@ -34,6 +38,8 @@ export default function ReportSetupView({ // Note: Geographic populations are no longer stored as user associations. // They are selected per-simulation. const userId = MOCK_USER_ID.toString(); + const countryId = useCurrentCountry(); + const { regions } = useRegions(countryId); const { data: householdData } = useUserHouseholds(userId); // Check if simulations are fully configured @@ -70,7 +76,7 @@ export default function ReportSetupView({ const setupConditionCards = [ { title: getBaselineCardTitle(simulation1, simulation1Configured), - description: getBaselineCardDescription(simulation1, simulation1Configured), + description: getBaselineCardDescription(simulation1, simulation1Configured, regions), onClick: handleSimulation1Select, isSelected: selectedCard === 'simulation1', isFulfilled: simulation1Configured, @@ -88,7 +94,8 @@ export default function ReportSetupView({ simulation2Configured, simulation1Configured, isSimulation2Optional, - !isPopulationDataLoaded + !isPopulationDataLoaded, + regions ), onClick: handleSimulation2Select, isSelected: selectedCard === 'simulation2', @@ -163,18 +170,38 @@ function getBaselineCardTitle( return 'Baseline simulation'; } +/** + * Get population display label for a simulation + */ +function getPopulationLabel( + simulation: SimulationStateProps | null, + regions: MetadataRegionEntry[] +): string { + if (simulation?.population.household?.id) { + return simulation.population.household.id; + } + if (simulation?.population.geography) { + const geo = simulation.population.geography; + if (isNationalGeography(geo)) { + return getCountryLabel(geo.countryId); + } + return getRegionLabel(geo.regionCode, regions); + } + return 'N/A'; +} + /** * Get description for baseline simulation card */ function getBaselineCardDescription( simulation: SimulationStateProps | null, - isConfigured: boolean + isConfigured: boolean, + regions: MetadataRegionEntry[] ): string { if (isConfigured) { const policyId = simulation?.policy.id || 'N/A'; - const populationId = - simulation?.population.household?.id || simulation?.population.geography?.regionCode || 'N/A'; - return `Policy #${policyId} • Household(s) ${populationId}`; + const populationLabel = getPopulationLabel(simulation, regions); + return `Policy #${policyId} • Household(s) ${populationLabel}`; } return 'Select your baseline simulation'; } @@ -214,14 +241,14 @@ function getComparisonCardDescription( isConfigured: boolean, baselineConfigured: boolean, isOptional: boolean, - dataLoading: boolean + dataLoading: boolean, + regions: MetadataRegionEntry[] ): string { // If configured, show simulation details if (isConfigured) { const policyId = simulation?.policy.id || 'N/A'; - const populationId = - simulation?.population.household?.id || simulation?.population.geography?.regionCode || 'N/A'; - return `Policy #${policyId} • Household(s) ${populationId}`; + const populationLabel = getPopulationLabel(simulation, regions); + return `Policy #${policyId} • Household(s) ${populationLabel}`; } // If baseline not configured yet, show waiting message diff --git a/app/src/pathways/report/views/ReportSimulationExistingView.tsx b/app/src/pathways/report/views/ReportSimulationExistingView.tsx index 4088a1a52..736253d78 100644 --- a/app/src/pathways/report/views/ReportSimulationExistingView.tsx +++ b/app/src/pathways/report/views/ReportSimulationExistingView.tsx @@ -2,8 +2,11 @@ import { useState } from 'react'; import { Text } from '@mantine/core'; import PathwayView from '@/components/common/PathwayView'; import { MOCK_USER_ID } from '@/constants'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useRegions } from '@/hooks/useRegions'; import { EnhancedUserSimulation, useUserSimulations } from '@/hooks/useUserSimulations'; import { SimulationStateProps } from '@/types/pathwayState'; +import { getCountryLabel, getRegionLabel, isNationalGeography } from '@/utils/geographyUtils'; import { arePopulationsCompatible } from '@/utils/populationCompatibility'; interface ReportSimulationExistingViewProps { @@ -24,10 +27,21 @@ export default function ReportSimulationExistingView({ onCancel, }: ReportSimulationExistingViewProps) { const userId = MOCK_USER_ID.toString(); + const countryId = useCurrentCountry(); + const { regions } = useRegions(countryId); const { data, isLoading, isError, error } = useUserSimulations(userId); const [localSimulation, setLocalSimulation] = useState(null); + // Helper to get geography display label + function getGeographyLabel(enhancedSim: EnhancedUserSimulation): string | undefined { + if (!enhancedSim.geography) return undefined; + if (isNationalGeography(enhancedSim.geography)) { + return getCountryLabel(enhancedSim.geography.countryId); + } + return getRegionLabel(enhancedSim.geography.regionCode, regions); + } + function canProceed() { if (!localSimulation) { return false; @@ -129,7 +143,7 @@ export default function ReportSimulationExistingView({ const policyLabel = enhancedSim.userPolicy?.label || enhancedSim.policy?.label || enhancedSim.policy?.id; const populationLabel = - enhancedSim.userHousehold?.label || enhancedSim.geography?.regionCode || simulation.populationId; + enhancedSim.userHousehold?.label || getGeographyLabel(enhancedSim) || simulation.populationId; if (policyLabel && populationLabel) { subtitle = subtitle diff --git a/app/src/pathways/report/views/simulation/SimulationSetupView.tsx b/app/src/pathways/report/views/simulation/SimulationSetupView.tsx index 807950d81..2fc0ef71a 100644 --- a/app/src/pathways/report/views/simulation/SimulationSetupView.tsx +++ b/app/src/pathways/report/views/simulation/SimulationSetupView.tsx @@ -6,7 +6,10 @@ import { useState } from 'react'; import PathwayView from '@/components/common/PathwayView'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useRegions } from '@/hooks/useRegions'; import { SimulationStateProps } from '@/types/pathwayState'; +import { getCountryLabel, getRegionLabel, isNationalGeography } from '@/utils/geographyUtils'; import { isPolicyConfigured, isPopulationConfigured, @@ -36,6 +39,8 @@ export default function SimulationSetupView({ onCancel, }: SimulationSetupViewProps) { const [selectedCard, setSelectedCard] = useState(null); + const countryId = useCurrentCountry(); + const { regions } = useRegions(countryId); const policy = simulation.policy; const population = simulation.population; @@ -64,6 +69,15 @@ export default function SimulationSetupView({ const canProceed: boolean = isPolicyConfigured(policy) && isPopulationConfigured(population); + // Helper to get geography display label + function getGeographyDisplayLabel(): string { + if (!population.geography) return ''; + if (isNationalGeography(population.geography)) { + return getCountryLabel(population.geography.countryId); + } + return getRegionLabel(population.geography.regionCode, regions); + } + function generatePopulationCardTitle() { if (!isPopulationConfigured(population)) { return 'Add household(s)'; @@ -81,7 +95,8 @@ export default function SimulationSetupView({ return `Household #${population.household.id}`; } if (population.geography) { - return `Household(s) (${population.geography.regionCode})`; + const geoLabel = getGeographyDisplayLabel(); + return `Household(s) (${geoLabel})`; } return ''; } @@ -93,7 +108,7 @@ export default function SimulationSetupView({ // In simulation 2 of a report, indicate population is inherited from baseline if (isSimulation2InReport) { - const popId = population.household?.id || population.geography?.regionCode; + const popId = population.household?.id || getGeographyDisplayLabel(); const popType = population.household ? 'Household' : 'Household collection'; return `${popType} ${popId} • Inherited from baseline simulation`; } @@ -102,7 +117,8 @@ export default function SimulationSetupView({ return `Household #${population.household.id}`; } if (population.label && population.geography) { - return `Household collection (${population.geography.regionCode})`; + const geoLabel = getGeographyDisplayLabel(); + return `Household collection (${geoLabel})`; } return ''; } diff --git a/app/src/tests/unit/pages/Simulations.page.test.tsx b/app/src/tests/unit/pages/Simulations.page.test.tsx index acc8913f0..0b8870dd7 100644 --- a/app/src/tests/unit/pages/Simulations.page.test.tsx +++ b/app/src/tests/unit/pages/Simulations.page.test.tsx @@ -28,6 +28,16 @@ vi.mock('@/hooks/useCurrentCountry', () => ({ useCurrentCountry: () => 'us', })); +// Mock useRegions +vi.mock('@/hooks/useRegions', () => ({ + useRegions: vi.fn(() => ({ + regions: [], + isLoading: false, + error: null, + rawRegions: [], + })), +})); + // Mock useNavigate const mockNavigate = vi.fn(); vi.mock('react-router-dom', async () => { diff --git a/app/src/tests/unit/pages/report-output/GeographySubPage.test.tsx b/app/src/tests/unit/pages/report-output/GeographySubPage.test.tsx index 86b0b6bde..f83342312 100644 --- a/app/src/tests/unit/pages/report-output/GeographySubPage.test.tsx +++ b/app/src/tests/unit/pages/report-output/GeographySubPage.test.tsx @@ -1,10 +1,29 @@ import { render, screen, within } from '@test-utils'; -import { describe, expect, test } from 'vitest'; +import { describe, expect, test, vi } from 'vitest'; import GeographySubPage from '@/pages/report-output/GeographySubPage'; import { mockGeographyCalifornia, mockGeographyNewYork, } from '@/tests/fixtures/pages/report-output/PopulationSubPage'; +import { MetadataRegionEntry } from '@/types/metadata'; + +// Mock regions data for V2 API labels +const mockRegions: MetadataRegionEntry[] = [ + { name: 'state/ca', label: 'California', type: 'state' }, + { name: 'state/ny', label: 'New York', type: 'state' }, + { name: 'ca', label: 'California', type: 'state' }, // Legacy format + { name: 'ny', label: 'New York', type: 'state' }, // Legacy format +]; + +// Mock useRegions hook +vi.mock('@/hooks/useRegions', () => ({ + useRegions: vi.fn(() => ({ + regions: mockRegions, + isLoading: false, + error: null, + rawRegions: [], + })), +})); describe('GeographySubPage - Design 4 Table Format', () => { describe('Empty and error states', () => { @@ -63,7 +82,7 @@ describe('GeographySubPage - Design 4 Table Format', () => { }); describe('Geography properties', () => { - test('given geography then displays all properties', () => { + test('given geography then displays all properties with V2 API labels', () => { render( { expect(screen.getByText(/geographic area/i)).toBeInTheDocument(); expect(screen.getByText(/type/i)).toBeInTheDocument(); - // Should display regionCode values (Phase 6.2 will add proper name lookup) - expect(screen.getByText('ca')).toBeInTheDocument(); - expect(screen.getByText('ny')).toBeInTheDocument(); - expect(screen.getAllByText('Subnational')).toHaveLength(2); // One for baseline, one for reform + // Should display human-readable labels from V2 API + expect(screen.getByText('California')).toBeInTheDocument(); + expect(screen.getByText('New York')).toBeInTheDocument(); + expect(screen.getAllByText('State')).toHaveLength(2); // Region type label }); test('given same geography then displays value once', () => { @@ -89,8 +108,8 @@ describe('GeographySubPage - Design 4 Table Format', () => { /> ); - // Should only show regionCode once per row - const caElements = screen.getAllByText('ca'); + // Should only show label once per row (merged column) + const caElements = screen.getAllByText('California'); expect(caElements.length).toBe(1); }); }); diff --git a/app/src/tests/unit/pathways/report/views/ReportSimulationExistingView.test.tsx b/app/src/tests/unit/pathways/report/views/ReportSimulationExistingView.test.tsx index 5eb366cc0..567401e34 100644 --- a/app/src/tests/unit/pathways/report/views/ReportSimulationExistingView.test.tsx +++ b/app/src/tests/unit/pathways/report/views/ReportSimulationExistingView.test.tsx @@ -16,6 +16,20 @@ import { resetAllMocks, } from '@/tests/fixtures/pathways/report/views/ReportViewMocks'; +// Mock hooks for country context and regions +vi.mock('@/hooks/useCurrentCountry', () => ({ + useCurrentCountry: vi.fn(() => 'us'), +})); + +vi.mock('@/hooks/useRegions', () => ({ + useRegions: vi.fn(() => ({ + regions: [], + isLoading: false, + error: null, + rawRegions: [], + })), +})); + vi.mock('@/hooks/useUserSimulations', () => ({ useUserSimulations: vi.fn(), })); diff --git a/app/src/tests/unit/pathways/report/views/simulation/SimulationSetupView.test.tsx b/app/src/tests/unit/pathways/report/views/simulation/SimulationSetupView.test.tsx index 771eb5096..e13c4e57d 100644 --- a/app/src/tests/unit/pathways/report/views/simulation/SimulationSetupView.test.tsx +++ b/app/src/tests/unit/pathways/report/views/simulation/SimulationSetupView.test.tsx @@ -14,6 +14,20 @@ import { resetAllMocks, } from '@/tests/fixtures/pathways/report/views/SimulationViewMocks'; +// Mock hooks for country context and regions +vi.mock('@/hooks/useCurrentCountry', () => ({ + useCurrentCountry: vi.fn(() => 'us'), +})); + +vi.mock('@/hooks/useRegions', () => ({ + useRegions: vi.fn(() => ({ + regions: [], + isLoading: false, + error: null, + rawRegions: [], + })), +})); + describe('SimulationSetupView', () => { beforeEach(() => { resetAllMocks(); diff --git a/app/src/utils/geographyUtils.ts b/app/src/utils/geographyUtils.ts index 2a216c485..3754eaf04 100644 --- a/app/src/utils/geographyUtils.ts +++ b/app/src/utils/geographyUtils.ts @@ -1,4 +1,5 @@ -import type { Geography, isNationalGeography } from '@/types/ingredients/Geography'; +import type { Geography } from '@/types/ingredients/Geography'; +import { isNationalGeography } from '@/types/ingredients/Geography'; import { MetadataRegionEntry } from '@/types/metadata'; import { UK_REGION_TYPES } from '@/types/regionTypes'; From 1562cef8174e0d086e7a3bfaf381d0afc4926bd5 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Fri, 13 Feb 2026 23:06:45 +0100 Subject: [PATCH 08/17] fix: Various fixes following implementation plan completion --- .../hooks/utils/useFetchReportIngredients.ts | 2 +- .../tests/fixtures/api/associationFixtures.ts | 2 - .../components/DefaultBaselineOptionMocks.ts | 43 +------------------ .../utils/isDefaultBaselineSimulationMocks.ts | 30 +++---------- .../report-output/PopulationSubPage.test.tsx | 24 +++++++++-- .../PopulationPathwayWrapper.test.tsx | 4 -- .../ReportSimulationSelectionView.test.tsx | 7 --- app/src/types/ingredients/index.ts | 2 +- app/src/utils/populationCompatibility.ts | 3 +- 9 files changed, 30 insertions(+), 87 deletions(-) diff --git a/app/src/hooks/utils/useFetchReportIngredients.ts b/app/src/hooks/utils/useFetchReportIngredients.ts index d70f8294a..c502d53d6 100644 --- a/app/src/hooks/utils/useFetchReportIngredients.ts +++ b/app/src/hooks/utils/useFetchReportIngredients.ts @@ -37,7 +37,7 @@ type GeographyOption = { name: string; label: string }; * Construct Geography objects from geography-type simulations * * Builds simplified Geography objects using regionCode from simulation's populationId. - * Display names are looked up from region metadata at render time (Phase 6.2). + * Display names are looked up from region metadata at render time via useRegions(). * * @param simulations - Array of simulations to extract geographies from * @param _geographyOptions - Deprecated: lookup now happens at display time diff --git a/app/src/tests/fixtures/api/associationFixtures.ts b/app/src/tests/fixtures/api/associationFixtures.ts index 83857a61f..ded9b9fcf 100644 --- a/app/src/tests/fixtures/api/associationFixtures.ts +++ b/app/src/tests/fixtures/api/associationFixtures.ts @@ -98,8 +98,6 @@ export const createMockReportAssociation = (overrides?: Partial): Us // Error messages export const ERROR_MESSAGES = { HOUSEHOLD_NOT_FOUND: (id: string) => `UserHousehold with id ${id} not found`, - GEOGRAPHY_NOT_FOUND: (userId: string, geographyId: string) => - `UserGeography with userId ${userId} and geographyId ${geographyId} not found`, POLICY_NOT_FOUND: (id: string) => `UserPolicy with id ${id} not found`, SIMULATION_NOT_FOUND: (id: string) => `UserSimulation with id ${id} not found`, REPORT_NOT_FOUND: (id: string) => `UserReport with id ${id} not found`, diff --git a/app/src/tests/fixtures/pathways/report/components/DefaultBaselineOptionMocks.ts b/app/src/tests/fixtures/pathways/report/components/DefaultBaselineOptionMocks.ts index 70774cacf..b6ad9e322 100644 --- a/app/src/tests/fixtures/pathways/report/components/DefaultBaselineOptionMocks.ts +++ b/app/src/tests/fixtures/pathways/report/components/DefaultBaselineOptionMocks.ts @@ -10,7 +10,6 @@ export const TEST_USER_ID = 'test-user-123'; export const TEST_CURRENT_LAW_ID = 1; export const TEST_SIMULATION_ID = 'sim-123'; export const TEST_EXISTING_SIMULATION_ID = 'existing-sim-456'; -export const TEST_GEOGRAPHY_ID = 'geo-789'; export const DEFAULT_BASELINE_LABELS = { US: 'United States current law for all households nationwide', @@ -34,13 +33,8 @@ export const mockExistingDefaultBaselineSimulation: any = { populationId: TEST_COUNTRIES.US, }, geography: { - id: 'geo-1', - userId: TEST_USER_ID, countryId: TEST_COUNTRIES.US, - geographyId: TEST_COUNTRIES.US, - scope: 'national', - label: 'US nationwide', - createdAt: '2024-01-15T10:00:00Z', + regionCode: TEST_COUNTRIES.US, }, }; @@ -61,13 +55,8 @@ export const mockNonDefaultSimulation: any = { populationId: TEST_COUNTRIES.US, }, geography: { - id: 'geo-2', - userId: TEST_USER_ID, countryId: TEST_COUNTRIES.US, - geographyId: TEST_COUNTRIES.US, - scope: 'national', - label: 'US nationwide', - createdAt: '2024-01-15T11:00:00Z', + regionCode: TEST_COUNTRIES.US, }, }; @@ -75,24 +64,6 @@ export const mockNonDefaultSimulation: any = { export const mockOnSelect = vi.fn(); export const mockOnClick = vi.fn(); -// Mock API responses -export const mockGeographyCreationResponse = { - id: TEST_GEOGRAPHY_ID, - userId: TEST_USER_ID, - countryId: TEST_COUNTRIES.US, - geographyId: TEST_COUNTRIES.US, - scope: 'national' as const, - label: 'US nationwide', - createdAt: new Date().toISOString(), -}; - -export const mockSimulationCreationResponse = { - status: 'ok' as const, - result: { - simulation_id: TEST_SIMULATION_ID, - }, -}; - // Helper to reset all mocks export const resetAllMocks = () => { mockOnSelect.mockClear(); @@ -128,16 +99,6 @@ export const mockUseUserSimulationsWithExisting = { getPolicyLabel: vi.fn(), } as any; -export const mockUseCreateGeographicAssociation = { - mutateAsync: vi.fn().mockResolvedValue(mockGeographyCreationResponse), - isPending: false, - isError: false, - error: null, - mutate: vi.fn(), - reset: vi.fn(), - status: 'idle' as const, -} as any; - export const mockUseCreateSimulation = { createSimulation: vi.fn(), isPending: false, diff --git a/app/src/tests/fixtures/utils/isDefaultBaselineSimulationMocks.ts b/app/src/tests/fixtures/utils/isDefaultBaselineSimulationMocks.ts index 0d37300b9..b188f876b 100644 --- a/app/src/tests/fixtures/utils/isDefaultBaselineSimulationMocks.ts +++ b/app/src/tests/fixtures/utils/isDefaultBaselineSimulationMocks.ts @@ -37,12 +37,8 @@ export const mockDefaultBaselineSimulation: any = { populationId: TEST_COUNTRIES.US, }, geography: { - id: 'geo-1', countryId: TEST_COUNTRIES.US, - geographyId: TEST_COUNTRIES.US, - scope: 'national', - label: 'US nationwide', - createdAt: '2024-01-15T10:00:00Z', + regionCode: TEST_COUNTRIES.US, }, }; @@ -64,12 +60,8 @@ export const mockCustomPolicySimulation: any = { populationId: TEST_COUNTRIES.US, }, geography: { - id: 'geo-2', countryId: TEST_COUNTRIES.US, - geographyId: TEST_COUNTRIES.US, - scope: 'national', - label: 'US nationwide', - createdAt: '2024-01-15T11:00:00Z', + regionCode: TEST_COUNTRIES.US, }, }; @@ -91,12 +83,8 @@ export const mockSubnationalSimulation: any = { populationId: 'state/ca', // Subnational }, geography: { - id: 'geo-3', countryId: TEST_COUNTRIES.US, - geographyId: 'state/ca', - scope: 'subnational', - label: 'California', - createdAt: '2024-01-15T12:00:00Z', + regionCode: 'state/ca', }, }; @@ -137,12 +125,8 @@ export const mockWrongLabelSimulation: any = { populationId: TEST_COUNTRIES.US, }, geography: { - id: 'geo-5', countryId: TEST_COUNTRIES.US, - geographyId: TEST_COUNTRIES.US, - scope: 'national', - label: 'US nationwide', - createdAt: '2024-01-15T14:00:00Z', + regionCode: TEST_COUNTRIES.US, }, }; @@ -176,11 +160,7 @@ export const mockUKDefaultBaselineSimulation: any = { populationId: TEST_COUNTRIES.UK, }, geography: { - id: 'geo-7', countryId: TEST_COUNTRIES.UK, - geographyId: TEST_COUNTRIES.UK, - scope: 'national', - label: 'UK nationwide', - createdAt: '2024-01-15T16:00:00Z', + regionCode: TEST_COUNTRIES.UK, }, }; diff --git a/app/src/tests/unit/pages/report-output/PopulationSubPage.test.tsx b/app/src/tests/unit/pages/report-output/PopulationSubPage.test.tsx index 8d6aafe17..637002da3 100644 --- a/app/src/tests/unit/pages/report-output/PopulationSubPage.test.tsx +++ b/app/src/tests/unit/pages/report-output/PopulationSubPage.test.tsx @@ -1,7 +1,23 @@ import { render, screen } from '@test-utils'; -import { describe, expect, test } from 'vitest'; +import { describe, expect, test, vi } from 'vitest'; import PopulationSubPage from '@/pages/report-output/PopulationSubPage'; import { createPopulationSubPageProps } from '@/tests/fixtures/pages/report-output/PopulationSubPage'; +import { MetadataRegionEntry } from '@/types/metadata'; + +// Mock regions data for V2 API labels (used by GeographySubPage) +const mockRegions: MetadataRegionEntry[] = [ + { name: 'ca', label: 'California', type: 'state' }, + { name: 'ny', label: 'New York', type: 'state' }, +]; + +vi.mock('@/hooks/useRegions', () => ({ + useRegions: vi.fn(() => ({ + regions: mockRegions, + isLoading: false, + error: null, + rawRegions: [], + })), +})); describe('PopulationSubPage - Design 4 Router', () => { describe('Routing logic', () => { @@ -55,9 +71,9 @@ describe('PopulationSubPage - Design 4 Router', () => { const props = createPopulationSubPageProps.geographyDifferent(); render(); - // Should display regionCodes (note: Phase 6.2 will add proper name lookup) - expect(screen.getByText('ca')).toBeInTheDocument(); // Baseline - expect(screen.getByText('ny')).toBeInTheDocument(); // Reform + // Should display human-readable labels from V2 API + expect(screen.getByText('California')).toBeInTheDocument(); // Baseline + expect(screen.getByText('New York')).toBeInTheDocument(); // Reform }); test('given missing household data then displays error in HouseholdSubPage', () => { diff --git a/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx b/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx index 776e59229..1b34da2e6 100644 --- a/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx +++ b/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx @@ -29,10 +29,6 @@ vi.mock('@/hooks/useUserHousehold', () => ({ useCreateHousehold: vi.fn(() => ({ createHousehold: vi.fn(), isPending: false })), })); -vi.mock('@/hooks/useUserGeographic', () => ({ - useCreateGeographicAssociation: vi.fn(() => ({ mutateAsync: vi.fn(), isPending: false })), -})); - vi.mock('@/hooks/usePathwayNavigation', () => ({ usePathwayNavigation: vi.fn(() => ({ mode: 'SCOPE', diff --git a/app/src/tests/unit/pathways/report/views/ReportSimulationSelectionView.test.tsx b/app/src/tests/unit/pathways/report/views/ReportSimulationSelectionView.test.tsx index 10e6ee093..f80f350f1 100644 --- a/app/src/tests/unit/pathways/report/views/ReportSimulationSelectionView.test.tsx +++ b/app/src/tests/unit/pathways/report/views/ReportSimulationSelectionView.test.tsx @@ -26,13 +26,6 @@ vi.mock('@/hooks/useCreateSimulation', () => ({ })), })); -vi.mock('@/hooks/useUserGeographic', () => ({ - useCreateGeographicAssociation: vi.fn(() => ({ - mutateAsync: vi.fn(), - isPending: false, - })), -})); - vi.mock('@/hooks/useUserHousehold', () => ({ useUserHouseholds: vi.fn(() => ({ data: [], isLoading: false })), })); diff --git a/app/src/types/ingredients/index.ts b/app/src/types/ingredients/index.ts index a3e0a4eb1..14db78369 100644 --- a/app/src/types/ingredients/index.ts +++ b/app/src/types/ingredients/index.ts @@ -40,7 +40,7 @@ export function isHousehold(obj: BaseIngredient): obj is Household { * Type guard to check if an object is a Geography */ export function isGeography(obj: BaseIngredient): obj is Geography { - return 'scope' in obj && 'geographyId' in obj && !('householdData' in obj); + return 'regionCode' in obj && 'countryId' in obj && !('householdData' in obj); } /** diff --git a/app/src/utils/populationCompatibility.ts b/app/src/utils/populationCompatibility.ts index 04b5060eb..4b2c67d39 100644 --- a/app/src/utils/populationCompatibility.ts +++ b/app/src/utils/populationCompatibility.ts @@ -47,8 +47,7 @@ export function getPopulationLabel(population: Population | null): string { return `Household #${population.household.id}`; } - // Third priority: geography region code - // TODO: Phase 6.2 will add proper lookup from region metadata + // Third priority: geography region code (fallback when region metadata unavailable) if (population.geography?.regionCode) { return population.geography.regionCode; } From 2c1c4153e986b93f149f3400879924b2be145cca Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 16 Feb 2026 18:33:04 +0100 Subject: [PATCH 09/17] feat: Migrate simulations to new geography structure, but maintain old sims API --- app/src/adapters/SimulationAdapter.ts | 10 +++++-- app/src/api/simulation.ts | 10 ++++++- .../pathways/report/views/ReportSetupView.tsx | 4 +-- .../views/population/PopulationLabelView.tsx | 9 +++--- .../SimulationPopulationSetupView.tsx | 20 +++++++++++-- .../views/simulation/SimulationSetupView.tsx | 6 ++-- app/src/tests/fixtures/api/simulationMocks.ts | 17 +++++------ .../unit/adapters/SimulationAdapter.test.ts | 6 ++-- app/src/tests/unit/api/simulation.test.ts | 30 +++++++++++++------ .../payloads/SimulationCreationPayload.ts | 14 +++++++-- 10 files changed, 85 insertions(+), 41 deletions(-) diff --git a/app/src/adapters/SimulationAdapter.ts b/app/src/adapters/SimulationAdapter.ts index 77c4ae5ad..18f1d0295 100644 --- a/app/src/adapters/SimulationAdapter.ts +++ b/app/src/adapters/SimulationAdapter.ts @@ -76,9 +76,15 @@ export class SimulationAdapter { throw new Error('Simulation must have a populationType'); } + // Map internal Simulation fields to V2-style payload + if (simulation.populationType === 'geography') { + return { + region: simulation.populationId, + policy_id: parseInt(simulation.policyId, 10), + }; + } return { - population_id: simulation.populationId, - population_type: simulation.populationType, + household_id: simulation.populationId, policy_id: parseInt(simulation.policyId, 10), }; } diff --git a/app/src/api/simulation.ts b/app/src/api/simulation.ts index 7d456681a..e10a59c92 100644 --- a/app/src/api/simulation.ts +++ b/app/src/api/simulation.ts @@ -44,13 +44,21 @@ export async function createSimulation( ): Promise<{ result: { simulation_id: string } }> { const url = `${BASE_URL}/${countryId}/simulation`; + // Translate V2-style payload to V1 wire format; note this is temporary + // until we migrate to v2 simulation in a future PR + const v1Payload = { + population_id: data.region ?? data.household_id, + population_type: data.region ? 'geography' : 'household', + policy_id: data.policy_id, + }; + const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, - body: JSON.stringify(data), + body: JSON.stringify(v1Payload), }); if (!response.ok) { diff --git a/app/src/pathways/report/views/ReportSetupView.tsx b/app/src/pathways/report/views/ReportSetupView.tsx index 93ce5ee38..94df79e72 100644 --- a/app/src/pathways/report/views/ReportSetupView.tsx +++ b/app/src/pathways/report/views/ReportSetupView.tsx @@ -201,7 +201,7 @@ function getBaselineCardDescription( if (isConfigured) { const policyId = simulation?.policy.id || 'N/A'; const populationLabel = getPopulationLabel(simulation, regions); - return `Policy #${policyId} • Household(s) ${populationLabel}`; + return `Policy #${policyId} • ${populationLabel}`; } return 'Select your baseline simulation'; } @@ -248,7 +248,7 @@ function getComparisonCardDescription( if (isConfigured) { const policyId = simulation?.policy.id || 'N/A'; const populationLabel = getPopulationLabel(simulation, regions); - return `Policy #${policyId} • Household(s) ${populationLabel}`; + return `Policy #${policyId} • ${populationLabel}`; } // If baseline not configured yet, show waiting message diff --git a/app/src/pathways/report/views/population/PopulationLabelView.tsx b/app/src/pathways/report/views/population/PopulationLabelView.tsx index 87bb1bcf4..a129b2043 100644 --- a/app/src/pathways/report/views/population/PopulationLabelView.tsx +++ b/app/src/pathways/report/views/population/PopulationLabelView.tsx @@ -8,10 +8,11 @@ import { useState } from 'react'; import { Stack, Text, TextInput } from '@mantine/core'; import PathwayView from '@/components/common/PathwayView'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useRegions } from '@/hooks/useRegions'; import { isNationalGeography } from '@/types/ingredients/Geography'; import { PathwayMode } from '@/types/pathwayModes/PathwayMode'; import { PopulationStateProps } from '@/types/pathwayState'; -import { extractRegionDisplayValue } from '@/utils/regionStrategies'; +import { getRegionLabel } from '@/utils/geographyUtils'; interface PopulationLabelViewProps { population: PopulationStateProps; @@ -38,6 +39,7 @@ export default function PopulationLabelView({ } const countryId = useCurrentCountry(); + const { regions } = useRegions(countryId); const initializeText = countryId === 'uk' ? 'Initialise' : 'Initialize'; // Initialize with existing label or generate a default based on population type @@ -51,9 +53,8 @@ export default function PopulationLabelView({ if (isNationalGeography(population.geography)) { return 'National Households'; } else if (population.geography.regionCode) { - // Use display value (strip prefix for UK regions) - const displayValue = extractRegionDisplayValue(population.geography.regionCode); - return `${displayValue} Households`; + const label = getRegionLabel(population.geography.regionCode, regions); + return `${label} Households`; } return 'Regional Households'; } diff --git a/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx b/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx index c5990f0cf..b74217a56 100644 --- a/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx +++ b/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx @@ -7,9 +7,12 @@ import { useState } from 'react'; import PathwayView from '@/components/common/PathwayView'; import { MOCK_USER_ID } from '@/constants'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useRegions } from '@/hooks/useRegions'; import { useUserHouseholds } from '@/hooks/useUserHousehold'; import { PopulationStateProps, SimulationStateProps } from '@/types/pathwayState'; -import { getPopulationLabel, getSimulationLabel } from '@/utils/populationCompatibility'; +import { getCountryLabel, getRegionLabel, isNationalGeography } from '@/utils/geographyUtils'; +import { getSimulationLabel } from '@/utils/populationCompatibility'; import { getPopulationLockConfig, getPopulationSelectionSubtitle, @@ -40,6 +43,8 @@ export default function SimulationPopulationSetupView({ onCancel, }: SimulationPopulationSetupViewProps) { const userId = MOCK_USER_ID.toString(); + const countryId = useCurrentCountry(); + const { regions } = useRegions(countryId); const { data: userHouseholds } = useUserHouseholds(userId); // Note: Geographic populations are no longer stored as user associations. // They are selected per-simulation. We only check for existing households. @@ -55,6 +60,17 @@ export default function SimulationPopulationSetupView({ otherPopulation as any ); + function getOtherPopulationLabel(): string { + if (otherPopulation?.label) return otherPopulation.label; + if (otherPopulation?.household?.id) return `Household #${otherPopulation.household.id}`; + if (otherPopulation?.geography) { + const geo = otherPopulation.geography; + if (isNationalGeography(geo)) return getCountryLabel(geo.countryId); + return getRegionLabel(geo.regionCode, regions); + } + return 'Unknown Household(s)'; + } + function handleClickCreateNew() { setSelectedAction('createNew'); } @@ -92,7 +108,7 @@ export default function SimulationPopulationSetupView({ // Card 2: Use Population from Other Simulation (enabled) { title: `Use household(s) from ${getSimulationLabel(otherSimulation as any)}`, - description: `Household(s): ${getPopulationLabel(otherPopulation as any)}`, + description: `Household(s): ${getOtherPopulationLabel()}`, onClick: handleClickCopyExisting, isSelected: selectedAction === 'copyExisting', isDisabled: false, diff --git a/app/src/pathways/report/views/simulation/SimulationSetupView.tsx b/app/src/pathways/report/views/simulation/SimulationSetupView.tsx index 2fc0ef71a..a0d4ba345 100644 --- a/app/src/pathways/report/views/simulation/SimulationSetupView.tsx +++ b/app/src/pathways/report/views/simulation/SimulationSetupView.tsx @@ -95,8 +95,7 @@ export default function SimulationSetupView({ return `Household #${population.household.id}`; } if (population.geography) { - const geoLabel = getGeographyDisplayLabel(); - return `Household(s) (${geoLabel})`; + return getGeographyDisplayLabel(); } return ''; } @@ -117,8 +116,7 @@ export default function SimulationSetupView({ return `Household #${population.household.id}`; } if (population.label && population.geography) { - const geoLabel = getGeographyDisplayLabel(); - return `Household collection (${geoLabel})`; + return getGeographyDisplayLabel(); } return ''; } diff --git a/app/src/tests/fixtures/api/simulationMocks.ts b/app/src/tests/fixtures/api/simulationMocks.ts index c4d8132ca..6ed9a0526 100644 --- a/app/src/tests/fixtures/api/simulationMocks.ts +++ b/app/src/tests/fixtures/api/simulationMocks.ts @@ -25,20 +25,17 @@ export const SIMULATION_IDS = { // Test payloads export const mockSimulationPayload: SimulationCreationPayload = { - population_id: '123', - population_type: 'household', + household_id: '123', policy_id: 456, }; export const mockSimulationPayloadGeography: SimulationCreationPayload = { - population_id: 'california', - population_type: 'geography', + region: 'california', policy_id: 789, }; export const mockSimulationPayloadMinimal: SimulationCreationPayload = { - population_id: 'household-minimal', - population_type: 'household', + household_id: 'household-minimal', policy_id: 1, }; @@ -47,8 +44,8 @@ export const mockSimulationMetadata: SimulationMetadata = { id: parseInt(SIMULATION_IDS.VALID, 10), country_id: TEST_COUNTRIES.US, api_version: '1.0.0', - population_id: mockSimulationPayload.population_id, - population_type: mockSimulationPayload.population_type!, + population_id: mockSimulationPayload.household_id!, + population_type: 'household', policy_id: mockSimulationPayload.policy_id.toString(), }; @@ -58,8 +55,8 @@ export const mockCreateSimulationSuccessResponse = { result: { id: parseInt(SIMULATION_IDS.NEW, 10), country_id: TEST_COUNTRIES.US, - population_id: mockSimulationPayload.population_id, - population_type: mockSimulationPayload.population_type, + population_id: mockSimulationPayload.household_id, + population_type: 'household', policy_id: mockSimulationPayload.policy_id, }, }; diff --git a/app/src/tests/unit/adapters/SimulationAdapter.test.ts b/app/src/tests/unit/adapters/SimulationAdapter.test.ts index cc2ba73f0..760c2ff99 100644 --- a/app/src/tests/unit/adapters/SimulationAdapter.test.ts +++ b/app/src/tests/unit/adapters/SimulationAdapter.test.ts @@ -105,8 +105,7 @@ describe('SimulationAdapter', () => { // Then expect(payload).toEqual({ - population_id: TEST_POPULATION_IDS.HOUSEHOLD_1, - population_type: 'household', + household_id: TEST_POPULATION_IDS.HOUSEHOLD_1, policy_id: 1, }); }); @@ -123,8 +122,7 @@ describe('SimulationAdapter', () => { // Then expect(payload).toEqual({ - population_id: TEST_POPULATION_IDS.GEOGRAPHY_US, - population_type: 'geography', + region: TEST_POPULATION_IDS.GEOGRAPHY_US, policy_id: 1, }); }); diff --git a/app/src/tests/unit/api/simulation.test.ts b/app/src/tests/unit/api/simulation.test.ts index 3c43a8c0e..3476cef04 100644 --- a/app/src/tests/unit/api/simulation.test.ts +++ b/app/src/tests/unit/api/simulation.test.ts @@ -49,14 +49,18 @@ describe('createSimulation', () => { // When const result = await createSimulation(TEST_COUNTRIES.US, mockSimulationPayload); - // Then + // Then - payload is translated to V1 wire format expect(mockFetch).toHaveBeenCalledWith(`${BASE_URL}/${TEST_COUNTRIES.US}/simulation`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, - body: JSON.stringify(mockSimulationPayload), + body: JSON.stringify({ + population_id: mockSimulationPayload.household_id, + population_type: 'household', + policy_id: mockSimulationPayload.policy_id, + }), }); expect(result).toEqual({ result: { @@ -72,8 +76,8 @@ describe('createSimulation', () => { ...mockCreateSimulationSuccessResponse, result: { ...mockCreateSimulationSuccessResponse.result, - population_id: mockSimulationPayloadGeography.population_id, - population_type: mockSimulationPayloadGeography.population_type, + population_id: mockSimulationPayloadGeography.region, + population_type: 'geography', }, }; mockFetch.mockResolvedValueOnce(mockSuccessResponse(geographyResponse) as any); @@ -81,14 +85,18 @@ describe('createSimulation', () => { // When const result = await createSimulation(TEST_COUNTRIES.US, mockSimulationPayloadGeography); - // Then + // Then - payload is translated to V1 wire format expect(mockFetch).toHaveBeenCalledWith(`${BASE_URL}/${TEST_COUNTRIES.US}/simulation`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, - body: JSON.stringify(mockSimulationPayloadGeography), + body: JSON.stringify({ + population_id: mockSimulationPayloadGeography.region, + population_type: 'geography', + policy_id: mockSimulationPayloadGeography.policy_id, + }), }); expect(result).toEqual({ result: { @@ -104,7 +112,7 @@ describe('createSimulation', () => { ...mockCreateSimulationSuccessResponse, result: { ...mockCreateSimulationSuccessResponse.result, - population_id: mockSimulationPayloadMinimal.population_id, + population_id: mockSimulationPayloadMinimal.household_id, policy_id: null, }, }; @@ -113,14 +121,18 @@ describe('createSimulation', () => { // When const result = await createSimulation(TEST_COUNTRIES.US, mockSimulationPayloadMinimal); - // Then + // Then - payload is translated to V1 wire format expect(mockFetch).toHaveBeenCalledWith(`${BASE_URL}/${TEST_COUNTRIES.US}/simulation`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, - body: JSON.stringify(mockSimulationPayloadMinimal), + body: JSON.stringify({ + population_id: mockSimulationPayloadMinimal.household_id, + population_type: 'household', + policy_id: mockSimulationPayloadMinimal.policy_id, + }), }); expect(result).toEqual({ result: { diff --git a/app/src/types/payloads/SimulationCreationPayload.ts b/app/src/types/payloads/SimulationCreationPayload.ts index 007521cbc..6c1460e0f 100644 --- a/app/src/types/payloads/SimulationCreationPayload.ts +++ b/app/src/types/payloads/SimulationCreationPayload.ts @@ -1,8 +1,16 @@ /** - * Payload format for creating a simulation via the API + * Payload format for creating a simulation. + * + * Uses V2 API semantics: `region` for geographic populations, + * `household_id` for household populations. Exactly one of + * `region` or `household_id` must be set. + * + * The API call layer translates this to V1 wire format + * (`population_id`/`population_type`) until simulation creation + * is fully migrated to V2. */ export interface SimulationCreationPayload { - population_id: string; - population_type: 'household' | 'geography'; + region?: string; // V2 region code (e.g., "state/ca", "us") + household_id?: string; // Household ID for household simulations policy_id: number; } From e132459d706f86dd7a655d649ee39576f265d6f1 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 16 Feb 2026 18:59:14 +0100 Subject: [PATCH 10/17] fix: Correctly modify household creation --- app/src/pages/Populations.page.tsx | 4 +- .../population/PopulationPathwayWrapper.tsx | 99 +++++-------------- .../views/population/HouseholdBuilderView.tsx | 14 ++- .../types/pathwayModes/PopulationViewMode.ts | 12 +-- 4 files changed, 39 insertions(+), 90 deletions(-) diff --git a/app/src/pages/Populations.page.tsx b/app/src/pages/Populations.page.tsx index f453fd464..9c006f666 100644 --- a/app/src/pages/Populations.page.tsx +++ b/app/src/pages/Populations.page.tsx @@ -173,8 +173,8 @@ export default function PopulationsPage() { void; @@ -34,73 +26,37 @@ export default function PopulationPathwayWrapper({ onComplete }: PopulationPathw const countryId = useCurrentCountry(); const navigate = useNavigate(); - // Initialize population state - const [populationState, setPopulationState] = useState(() => { - return initializePopulationState(); + const [populationState, setPopulationState] = useState({ + type: 'household', + label: null, + household: null, + geography: null, }); - // Get metadata for views - const metadata = useSelector((state: RootState) => state.metadata); - const regionData = useRegionsList(countryId); - - // ========== NAVIGATION ========== const { currentMode, navigateToMode, goBack, canGoBack } = usePathwayNavigation( - StandalonePopulationViewMode.SCOPE + StandalonePopulationViewMode.LABEL ); - // ========== CALLBACKS ========== - // Use shared callback factory with onPopulationComplete for standalone navigation - const populationCallbacks = createPopulationCallbacks( - setPopulationState, - (state) => state, // populationSelector: return the state itself (PopulationStateProps) - (_state, population) => population, // populationUpdater: replace entire state - navigateToMode, - StandalonePopulationViewMode.GEOGRAPHIC_CONFIRM, // returnMode (not used in standalone mode) - StandalonePopulationViewMode.LABEL, // labelMode - { - // Custom navigation for standalone pathway: exit to households list - onHouseholdComplete: (_householdId: string, _household: Household) => { - navigate(`/${countryId}/households`); - onComplete?.(); - }, - onGeographyComplete: (_geographyId: string, _label: string) => { - navigate(`/${countryId}/households`); - onComplete?.(); - }, - } - ); + const handleUpdateLabel = (label: string) => { + setPopulationState((prev) => ({ ...prev, label })); + }; + + const handleHouseholdSubmitSuccess = (_householdId: string, _household: Household) => { + navigate(`/${countryId}/households`); + onComplete?.(); + }; - // ========== VIEW RENDERING ========== let currentView: React.ReactElement; switch (currentMode) { - case StandalonePopulationViewMode.SCOPE: - currentView = ( - navigate(`/${countryId}/households`)} - /> - ); - break; - case StandalonePopulationViewMode.LABEL: currentView = ( { - // Navigate based on population type - if (populationState.type === 'household') { - navigateToMode(StandalonePopulationViewMode.HOUSEHOLD_BUILDER); - } else { - navigateToMode(StandalonePopulationViewMode.GEOGRAPHIC_CONFIRM); - } - }} - onBack={canGoBack ? goBack : undefined} + onUpdateLabel={handleUpdateLabel} + onNext={() => navigateToMode(StandalonePopulationViewMode.HOUSEHOLD_BUILDER)} + onBack={() => navigate(`/${countryId}/households`)} /> ); break; @@ -110,18 +66,7 @@ export default function PopulationPathwayWrapper({ onComplete }: PopulationPathw - ); - break; - - case StandalonePopulationViewMode.GEOGRAPHIC_CONFIRM: - currentView = ( - ); diff --git a/app/src/pathways/report/views/population/HouseholdBuilderView.tsx b/app/src/pathways/report/views/population/HouseholdBuilderView.tsx index bcbda5386..c1641af3a 100644 --- a/app/src/pathways/report/views/population/HouseholdBuilderView.tsx +++ b/app/src/pathways/report/views/population/HouseholdBuilderView.tsx @@ -4,7 +4,7 @@ * Props-based instead of Redux-based */ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { LoadingOverlay, Stack, Text } from '@mantine/core'; import { HouseholdAdapter } from '@/adapters/HouseholdAdapter'; @@ -13,6 +13,7 @@ import HouseholdBuilderForm from '@/components/household/HouseholdBuilderForm'; import { useBasicInputFields } from '@/hooks/useBasicInputFields'; import { useCreateHousehold } from '@/hooks/useCreateHousehold'; import { useReportYear } from '@/hooks/useReportYear'; +import { useEntities } from '@/hooks/useStaticMetadata'; import { RootState } from '@/store'; import { Household } from '@/types/ingredients/Household'; import { PopulationStateProps } from '@/types/pathwayState'; @@ -37,8 +38,15 @@ export default function HouseholdBuilderView({ // Get metadata-driven options const basicInputFields = useBasicInputFields(countryId); - const metadata = useSelector((state: RootState) => state.metadata); - const { loading, error } = metadata; + const reduxMetadata = useSelector((state: RootState) => state.metadata); + const entities = useEntities(countryId); + const { loading, error } = reduxMetadata; + + // Merge static entities into metadata so VariableResolver can resolve entity types + const metadata = useMemo( + () => ({ ...reduxMetadata, entities }), + [reduxMetadata, entities] + ); // Get all basic non-person fields dynamically (country-agnostic) // This handles US entities (tax_unit, spm_unit, etc.) and UK entities (benunit) automatically diff --git a/app/src/types/pathwayModes/PopulationViewMode.ts b/app/src/types/pathwayModes/PopulationViewMode.ts index 83652419c..dee574eb6 100644 --- a/app/src/types/pathwayModes/PopulationViewMode.ts +++ b/app/src/types/pathwayModes/PopulationViewMode.ts @@ -1,19 +1,15 @@ /** - * StandalonePopulationViewMode - Enum for standalone population creation pathway view states + * StandalonePopulationViewMode - Enum for standalone household creation pathway view states * * This is used by the standalone PopulationPathwayWrapper. * For population modes used within composite pathways (Report, Simulation), * see PopulationViewMode in SharedViewModes.ts * - * Maps to the frames in PopulationCreationFlow: - * - SCOPE: SelectGeographicScopeFrame (choose household vs geographic scope) - * - LABEL: SetPopulationLabelFrame (enter population name) - * - HOUSEHOLD_BUILDER: HouseholdBuilderFrame (configure household members) - * - GEOGRAPHIC_CONFIRM: GeographicConfirmationFrame (confirm geographic population) + * Two-step flow: + * - LABEL: Name the household + * - HOUSEHOLD_BUILDER: Configure household members */ export enum StandalonePopulationViewMode { - SCOPE = 'SCOPE', LABEL = 'LABEL', HOUSEHOLD_BUILDER = 'HOUSEHOLD_BUILDER', - GEOGRAPHIC_CONFIRM = 'GEOGRAPHIC_CONFIRM', } From 75321b4a7962ddacf00189b5f4e8c2126d6b60a6 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 16 Feb 2026 19:24:05 +0100 Subject: [PATCH 11/17] fix: Shift to new wording around geographies --- app/src/pages/Simulations.page.tsx | 4 +-- .../pathways/report/views/ReportSetupView.tsx | 10 +++--- .../views/ReportSimulationExistingView.tsx | 10 +++--- .../report/views/ReportSubmitView.tsx | 4 ++- .../population/GeographicConfirmationView.tsx | 36 ++++--------------- .../views/population/PopulationLabelView.tsx | 6 ++-- .../SimulationPopulationSetupView.tsx | 8 +++-- .../views/simulation/SimulationSetupView.tsx | 13 +++---- .../views/simulation/SimulationSubmitView.tsx | 12 +++---- .../pathwayCallbacks/populationCallbacks.ts | 2 +- app/src/utils/populationCompatibility.ts | 4 +-- 11 files changed, 43 insertions(+), 66 deletions(-) diff --git a/app/src/pages/Simulations.page.tsx b/app/src/pages/Simulations.page.tsx index cdbe459fb..073cea508 100644 --- a/app/src/pages/Simulations.page.tsx +++ b/app/src/pages/Simulations.page.tsx @@ -133,9 +133,7 @@ export default function SimulationsPage() { text: item.userHousehold?.label || (item.geography - ? isNationalGeography(item.geography) - ? getCountryLabel(item.geography.countryId) - : getRegionLabel(item.geography.regionCode, regions) + ? `Households in ${isNationalGeography(item.geography) ? getCountryLabel(item.geography.countryId) : getRegionLabel(item.geography.regionCode, regions)}` : null) || (item.household ? `Household #${item.household.id}` : 'No population'), } as TextValue, diff --git a/app/src/pathways/report/views/ReportSetupView.tsx b/app/src/pathways/report/views/ReportSetupView.tsx index 94df79e72..a64be18ea 100644 --- a/app/src/pathways/report/views/ReportSetupView.tsx +++ b/app/src/pathways/report/views/ReportSetupView.tsx @@ -178,14 +178,14 @@ function getPopulationLabel( regions: MetadataRegionEntry[] ): string { if (simulation?.population.household?.id) { - return simulation.population.household.id; + return `Household #${simulation.population.household.id}`; } if (simulation?.population.geography) { const geo = simulation.population.geography; - if (isNationalGeography(geo)) { - return getCountryLabel(geo.countryId); - } - return getRegionLabel(geo.regionCode, regions); + const label = isNationalGeography(geo) + ? getCountryLabel(geo.countryId) + : getRegionLabel(geo.regionCode, regions); + return `Households in ${label}`; } return 'N/A'; } diff --git a/app/src/pathways/report/views/ReportSimulationExistingView.tsx b/app/src/pathways/report/views/ReportSimulationExistingView.tsx index 736253d78..c9b43fc76 100644 --- a/app/src/pathways/report/views/ReportSimulationExistingView.tsx +++ b/app/src/pathways/report/views/ReportSimulationExistingView.tsx @@ -33,13 +33,13 @@ export default function ReportSimulationExistingView({ const { data, isLoading, isError, error } = useUserSimulations(userId); const [localSimulation, setLocalSimulation] = useState(null); - // Helper to get geography display label + // Helper to get geography display label in "Households in {label}" format function getGeographyLabel(enhancedSim: EnhancedUserSimulation): string | undefined { if (!enhancedSim.geography) return undefined; - if (isNationalGeography(enhancedSim.geography)) { - return getCountryLabel(enhancedSim.geography.countryId); - } - return getRegionLabel(enhancedSim.geography.regionCode, regions); + const label = isNationalGeography(enhancedSim.geography) + ? getCountryLabel(enhancedSim.geography.countryId) + : getRegionLabel(enhancedSim.geography.regionCode, regions); + return `Households in ${label}`; } function canProceed() { diff --git a/app/src/pathways/report/views/ReportSubmitView.tsx b/app/src/pathways/report/views/ReportSubmitView.tsx index 08161d840..aa18a1087 100644 --- a/app/src/pathways/report/views/ReportSubmitView.tsx +++ b/app/src/pathways/report/views/ReportSubmitView.tsx @@ -31,7 +31,9 @@ export default function ReportSubmitView({ // Get population label - use label if available, otherwise fall back to ID const populationLabel = simulation.population.label || - `Population #${simulation.population.household?.id || simulation.population.geography?.regionCode}`; + (simulation.population.household?.id ? `Household #${simulation.population.household.id}` : null) || + (simulation.population.geography?.regionCode ? `Households in ${simulation.population.geography.regionCode}` : null) || + 'No population'; return `${policyLabel} • ${populationLabel}`; }; diff --git a/app/src/pathways/report/views/population/GeographicConfirmationView.tsx b/app/src/pathways/report/views/population/GeographicConfirmationView.tsx index 94505cf31..32a52ca3a 100644 --- a/app/src/pathways/report/views/population/GeographicConfirmationView.tsx +++ b/app/src/pathways/report/views/population/GeographicConfirmationView.tsx @@ -11,7 +11,7 @@ import PathwayView from '@/components/common/PathwayView'; import { isNationalGeography } from '@/types/ingredients/Geography'; import { MetadataRegionEntry } from '@/types/metadata'; import { PopulationStateProps } from '@/types/pathwayState'; -import { getCountryLabel, getRegionLabel, getRegionTypeLabel } from '@/utils/geographyUtils'; +import { getCountryLabel, getRegionLabel } from '@/utils/geographyUtils'; interface GeographicConfirmationViewProps { population: PopulationStateProps; @@ -49,49 +49,27 @@ export default function GeographicConfirmationView({ const geographyCountryId = population.geography.countryId; const regionCode = population.geography.regionCode; - if (isNationalGeography(population.geography)) { - return ( - - - Confirm household collection - - - Scope: National - - - Country: {getCountryLabel(geographyCountryId)} - - - ); - } - - // Subnational - const regionLabel = getRegionLabel(regionCode, regions); - const regionTypeName = getRegionTypeLabel(geographyCountryId, regionCode, regions); + const label = isNationalGeography(population.geography) + ? getCountryLabel(geographyCountryId) + : getRegionLabel(regionCode, regions); return ( - Confirm household collection - - - Scope: {regionTypeName} - - - {regionTypeName}: {regionLabel} + Households in {label} ); }; const primaryAction = { - label: 'Create household collection', + label: 'Confirm', onClick: handleSubmit, }; return ( ( (geography: Geography | null, _scopeType: string, regionLabel?: string) => { // If geography is selected, complete immediately with auto-generated label if (geography) { - const label = regionLabel ? `${regionLabel} households` : 'Geographic households'; + const label = regionLabel ? `Households in ${regionLabel}` : 'Geographic households'; setState((prev) => { const population = populationSelector(prev); return populationUpdater(prev, { diff --git a/app/src/utils/populationCompatibility.ts b/app/src/utils/populationCompatibility.ts index 4b2c67d39..5b24a4079 100644 --- a/app/src/utils/populationCompatibility.ts +++ b/app/src/utils/populationCompatibility.ts @@ -49,10 +49,10 @@ export function getPopulationLabel(population: Population | null): string { // Third priority: geography region code (fallback when region metadata unavailable) if (population.geography?.regionCode) { - return population.geography.regionCode; + return `Households in ${population.geography.regionCode}`; } - return 'Unknown Household(s)'; + return 'Unknown household(s)'; } /** From 9c5487c86441d51dc266df8d817909f6da2a0e00 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 16 Feb 2026 19:44:48 +0100 Subject: [PATCH 12/17] test: Add tests --- .../PopulationPathwayWrapperMocks.ts | 32 ++++++++++ .../report/views/PopulationViewMocks.ts | 10 +++- .../PopulationPathwayWrapper.test.tsx | 60 +++++++++++-------- .../population/PopulationLabelView.test.tsx | 7 ++- .../utils/populationCompatibility.test.ts | 6 +- 5 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 app/src/tests/fixtures/pathways/population/PopulationPathwayWrapperMocks.ts diff --git a/app/src/tests/fixtures/pathways/population/PopulationPathwayWrapperMocks.ts b/app/src/tests/fixtures/pathways/population/PopulationPathwayWrapperMocks.ts new file mode 100644 index 000000000..e6393e77c --- /dev/null +++ b/app/src/tests/fixtures/pathways/population/PopulationPathwayWrapperMocks.ts @@ -0,0 +1,32 @@ +import { vi } from 'vitest'; + +// Test constants +export const TEST_COUNTRY_ID = 'us'; + +export const mockUseParams = { countryId: TEST_COUNTRY_ID }; + +// Mock callbacks +export const mockNavigate = vi.fn(); +export const mockNavigateToMode = vi.fn(); +export const mockGoBack = vi.fn(); + +// Mock hook return values +export const mockUsePathwayNavigationReturn = { + currentMode: 'LABEL', + navigateToMode: mockNavigateToMode, + goBack: mockGoBack, + canGoBack: false, +}; + +export const mockUseRegionsEmpty = { + regions: [], + isLoading: false, + error: null, + rawRegions: [], +}; + +export function resetAllMocks() { + mockNavigate.mockClear(); + mockNavigateToMode.mockClear(); + mockGoBack.mockClear(); +} diff --git a/app/src/tests/fixtures/pathways/report/views/PopulationViewMocks.ts b/app/src/tests/fixtures/pathways/report/views/PopulationViewMocks.ts index b1860e154..768ee5731 100644 --- a/app/src/tests/fixtures/pathways/report/views/PopulationViewMocks.ts +++ b/app/src/tests/fixtures/pathways/report/views/PopulationViewMocks.ts @@ -31,7 +31,7 @@ export const mockPopulationStateWithHousehold: PopulationStateProps = { }; export const mockPopulationStateWithGeography: PopulationStateProps = { - label: 'National Households', + label: null, type: 'geography', household: null, geography: { @@ -45,6 +45,14 @@ export const mockRegionData: any[] = [ { name: 'California', code: 'ca', geography_id: 'us_ca' }, ]; +// Mock return value for useRegions hook (empty regions) +export const mockUseRegionsEmpty = { + regions: [], + isLoading: false, + error: null, + rawRegions: [], +}; + export function resetAllMocks() { mockOnUpdateLabel.mockClear(); mockOnNext.mockClear(); diff --git a/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx b/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx index 1b34da2e6..5aaa5c907 100644 --- a/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx +++ b/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx @@ -3,10 +3,14 @@ import { useParams } from 'react-router-dom'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import PopulationPathwayWrapper from '@/pathways/population/PopulationPathwayWrapper'; - -const mockNavigate = vi.fn(); -const mockUseParams = { countryId: 'us' }; -const mockMetadata = { currentLawId: 1, economyOptions: { region: [] } }; +import { + mockNavigate, + mockUsePathwayNavigationReturn, + mockUseRegionsEmpty, + mockUseParams, + resetAllMocks, + TEST_COUNTRY_ID, +} from '@/tests/fixtures/pathways/population/PopulationPathwayWrapperMocks'; vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); @@ -17,44 +21,48 @@ vi.mock('react-router-dom', async () => { }; }); -vi.mock('react-redux', async () => { - const actual = await vi.importActual('react-redux'); - return { - ...actual, - useSelector: vi.fn(() => mockMetadata), - }; -}); - -vi.mock('@/hooks/useUserHousehold', () => ({ - useCreateHousehold: vi.fn(() => ({ createHousehold: vi.fn(), isPending: false })), -})); - vi.mock('@/hooks/usePathwayNavigation', () => ({ - usePathwayNavigation: vi.fn(() => ({ - mode: 'SCOPE', - navigateToMode: vi.fn(), - goBack: vi.fn(), - })), + usePathwayNavigation: vi.fn(() => mockUsePathwayNavigationReturn), })); vi.mock('@/hooks/useCurrentCountry', () => ({ useCurrentCountry: vi.fn(), })); +vi.mock('@/hooks/useRegions', () => ({ + useRegions: vi.fn(() => mockUseRegionsEmpty), +})); + describe('PopulationPathwayWrapper', () => { beforeEach(() => { + resetAllMocks(); vi.clearAllMocks(); vi.mocked(useParams).mockReturnValue(mockUseParams); - vi.mocked(useCurrentCountry).mockReturnValue('us'); + vi.mocked(useCurrentCountry).mockReturnValue(TEST_COUNTRY_ID); + }); + + test('given valid countryId then renders LABEL view', () => { + // When + render(); + + // Then - PopulationLabelView renders with "Name your household(s)" heading + expect(screen.getByRole('heading', { name: /name your household/i })).toBeInTheDocument(); + }); + + test('given LABEL view then displays household label input', () => { + // When + render(); + + // Then + expect(screen.getByLabelText(/household label/i)).toBeInTheDocument(); }); - test('given valid countryId then renders without error', () => { + test('given LABEL view then shows back button that navigates to households page', () => { // When - const { container } = render(); + render(); // Then - expect(container).toBeInTheDocument(); - expect(screen.queryByText(/error/i)).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: /back/i })).toBeInTheDocument(); }); test('given missing countryId then throws error', () => { diff --git a/app/src/tests/unit/pathways/report/views/population/PopulationLabelView.test.tsx b/app/src/tests/unit/pathways/report/views/population/PopulationLabelView.test.tsx index 16b61f48f..b4ed97bde 100644 --- a/app/src/tests/unit/pathways/report/views/population/PopulationLabelView.test.tsx +++ b/app/src/tests/unit/pathways/report/views/population/PopulationLabelView.test.tsx @@ -9,6 +9,7 @@ import { mockPopulationStateEmpty, mockPopulationStateWithGeography, mockPopulationStateWithHousehold, + mockUseRegionsEmpty, resetAllMocks, TEST_COUNTRY_ID, TEST_POPULATION_LABEL, @@ -18,6 +19,10 @@ vi.mock('@/hooks/useCurrentCountry', () => ({ useCurrentCountry: vi.fn(), })); +vi.mock('@/hooks/useRegions', () => ({ + useRegions: vi.fn(() => mockUseRegionsEmpty), +})); + describe('PopulationLabelView', () => { beforeEach(() => { resetAllMocks(); @@ -85,7 +90,7 @@ describe('PopulationLabelView', () => { ); // Then - expect(screen.getByLabelText(/household label/i)).toHaveValue('National Households'); + expect(screen.getByLabelText(/household label/i)).toHaveValue('Households nationwide'); }); test('given existing label then shows that label', () => { diff --git a/app/src/tests/unit/utils/populationCompatibility.test.ts b/app/src/tests/unit/utils/populationCompatibility.test.ts index c33d34dfb..fc4889e81 100644 --- a/app/src/tests/unit/utils/populationCompatibility.test.ts +++ b/app/src/tests/unit/utils/populationCompatibility.test.ts @@ -113,8 +113,8 @@ describe('populationCompatibility', () => { // When const result = getPopulationLabel(population); - // Then - Note: With simplified Geography type, regionCode is returned as fallback - expect(result).toBe('us-ca'); + // Then - With "Households in" prefix format + expect(result).toBe('Households in us-ca'); }); it('given population with label prioritizes label over household ID', () => { @@ -141,7 +141,7 @@ describe('populationCompatibility', () => { const result = getPopulationLabel(population); // Then - expect(result).toBe('Unknown Household(s)'); + expect(result).toBe('Unknown household(s)'); }); }); From e964ff3efe6a9bccdb6c7913524da1db48f25c4a Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 16 Feb 2026 19:54:23 +0100 Subject: [PATCH 13/17] chore: Clarify pathway flow in comment to retrigger CI Co-Authored-By: Claude Opus 4.6 --- app/src/pathways/population/PopulationPathwayWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/pathways/population/PopulationPathwayWrapper.tsx b/app/src/pathways/population/PopulationPathwayWrapper.tsx index fdefbef14..23e104476 100644 --- a/app/src/pathways/population/PopulationPathwayWrapper.tsx +++ b/app/src/pathways/population/PopulationPathwayWrapper.tsx @@ -1,7 +1,7 @@ /** * PopulationPathwayWrapper - Pathway orchestrator for standalone household creation * - * Two-step flow: name the household, then build it. + * Two-step flow: LABEL (name the household) → HOUSEHOLD_BUILDER (configure members). * Geography selection is only available through the report/simulation pathways. */ From 99c9d5f7cfea3fb46fbc1f11dda0dd0d72970abd Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 16 Feb 2026 19:59:56 +0100 Subject: [PATCH 14/17] ci: Remove base branch filter from PR workflow Allow PR checks to run for PRs targeting any branch, not just main/master. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pr.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 6a41b011e..48071c29b 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -2,9 +2,6 @@ name: PR checks on: pull_request: - branches: - - main - - master jobs: lint: From 6d0545dacff49fdec9f7df7bd21fb66fec16abba Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 16 Feb 2026 20:16:17 +0100 Subject: [PATCH 15/17] chore: Lint --- .../stronger-start-working-families-act.md | 14 +++++++------- .../articles/utah-sb60-income-tax-reduction.md | 10 +++++----- app/src/hooks/useUserReports.ts | 6 ------ app/src/hooks/useUserSimulations.ts | 4 ---- app/src/pages/Populations.page.tsx | 7 +------ app/src/pages/Simulations.page.tsx | 2 +- .../pages/report-output/GeographySubPage.tsx | 5 ++++- .../pages/report-output/PopulationSubPage.tsx | 5 +---- .../population/PopulationPathwayWrapper.tsx | 2 +- .../pathways/report/ReportPathwayWrapper.tsx | 4 ---- .../pathways/report/views/ReportSetupView.tsx | 2 +- .../views/ReportSimulationExistingView.tsx | 7 ++++--- .../pathways/report/views/ReportSubmitView.tsx | 8 ++++++-- .../views/population/HouseholdBuilderView.tsx | 5 +---- .../population/PopulationExistingView.tsx | 7 +------ .../views/population/PopulationScopeView.tsx | 18 +++++++++++++++--- .../SimulationPopulationSetupView.tsx | 8 ++++++-- .../views/simulation/SimulationSetupView.tsx | 4 +++- .../views/simulation/SimulationSubmitView.tsx | 16 +++++++++++----- .../simulation/SimulationPathwayWrapper.tsx | 2 -- .../components/common/LazyNestedMenu.test.tsx | 4 ---- .../unit/hooks/useLazyParameterTree.test.ts | 3 --- .../tests/unit/hooks/useStaticMetadata.test.ts | 2 +- .../PopulationPathwayWrapper.test.tsx | 2 +- app/src/types/ingredients/index.ts | 8 +------- app/src/utils/geographyUtils.ts | 3 +-- .../pathwayCallbacks/populationCallbacks.ts | 10 +++++++++- .../utils/validation/ingredientValidation.ts | 1 - 28 files changed, 81 insertions(+), 88 deletions(-) diff --git a/app/src/data/posts/articles/stronger-start-working-families-act.md b/app/src/data/posts/articles/stronger-start-working-families-act.md index e09db0061..0ebde93e3 100644 --- a/app/src/data/posts/articles/stronger-start-working-families-act.md +++ b/app/src/data/posts/articles/stronger-start-working-families-act.md @@ -4,18 +4,18 @@ We at PolicyEngine have analyzed the effects of this proposed change. Key results: -* Costs $14.6 billion over ten years (2026-2035) -* Benefits 5.9% of Americans -* Reduces child poverty by 0.4% -* Lowers the Gini index of inequality by 0.024% +- Costs $14.6 billion over ten years (2026-2035) +- Benefits 5.9% of Americans +- Reduces child poverty by 0.4% +- Lowers the Gini index of inequality by 0.024% -*[Use PolicyEngine](https://www.policyengine.org/us) to view the full results or calculate the effect on your household.* +_[Use PolicyEngine](https://www.policyengine.org/us) to view the full results or calculate the effect on your household._ ## Background The Child Tax Credit provides up to $2,200 per qualifying child, with up to $1,700 of that amount refundable as the Additional Child Tax Credit (ACTC) in 2026. The refundable portion phases in at 15% of earned income above $2,500, which means families with lower earnings may not receive the full refundable credit. -The Stronger Start for Working Families Act would eliminate this $2,500 threshold, meaning families would begin receiving the refundable CTC from their first dollar of earned income. For example, a single parent with one child currently needs to earn $13,833 to receive the full $1,700 refundable credit ($1,700 ÷ 15% + $2,500 = $13,833). Under the reform, they would only need $11,333 of earnings to reach the maximum refundable amount ($1,700 ÷ 15% = $11,333). Figure 1 illustrates this phase-in pattern for current law and the proposed reform. +The Stronger Start for Working Families Act would eliminate this $2,500 threshold, meaning families would begin receiving the refundable CTC from their first dollar of earned income. For example, a single parent with one child currently needs to earn $13,833 to receive the full $1,700 refundable credit ($1,700 ÷ 15% + $2,500 = $13,833). Under the reform, they would only need $11,333 of earnings to reach the maximum refundable amount ($1,700 ÷ 15% = $11,333). Figure 1 illustrates this phase-in pattern for current law and the proposed reform. @@ -25,7 +25,7 @@ The reform primarily benefits lower-income families with children who do not rec The maximum benefit any household can receive is $375, regardless of the number of children (15% × $2,500 = $375). Household benefits begins phasing out once families reach their maximum refundable credit amount. For the single parent with two children, this phase-out occurs between $22,667 and $25,167 of employment income. Since the credit phases in at a flat 15% rate rather than 15% per child, households with more children reach their maximum refundable amount at greater income levels, pushing the phase-out range higher. Figure 2 displays the change in net income for households as earnings rise, based on the number of children and filing status.[^1] -[^1]: Household filing status does not affect net income under this reform, as the phase-in range is universal among households and net income is affected only by each household's number of children. +[^1]: Household filing status does not affect net income under this reform, as the phase-in range is universal among households and net income is affected only by each household's number of children. diff --git a/app/src/data/posts/articles/utah-sb60-income-tax-reduction.md b/app/src/data/posts/articles/utah-sb60-income-tax-reduction.md index e2a2c612d..fe9eb3ac2 100644 --- a/app/src/data/posts/articles/utah-sb60-income-tax-reduction.md +++ b/app/src/data/posts/articles/utah-sb60-income-tax-reduction.md @@ -4,12 +4,12 @@ We at PolicyEngine have analyzed the effects of this proposed change on the stat Key results for 2026: -* Reduces state revenues by $83.6 million -* Benefits 53.2% of Utah residents -* Has no effect on the Supplemental Poverty Measure -* Raises the Gini index of inequality by 0.01% +- Reduces state revenues by $83.6 million +- Benefits 53.2% of Utah residents +- Has no effect on the Supplemental Poverty Measure +- Raises the Gini index of inequality by 0.01% -*[Use PolicyEngine](https://www.policyengine.org/us) to view the full results or calculate the effect on your household.* +_[Use PolicyEngine](https://www.policyengine.org/us) to view the full results or calculate the effect on your household._ ## Tax reform diff --git a/app/src/hooks/useUserReports.ts b/app/src/hooks/useUserReports.ts index cb7de1d1a..883f7c768 100644 --- a/app/src/hooks/useUserReports.ts +++ b/app/src/hooks/useUserReports.ts @@ -16,7 +16,6 @@ import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation'; import { UserReport } from '@/types/ingredients/UserReport'; import { UserSimulation } from '@/types/ingredients/UserSimulation'; import { householdKeys, policyKeys, reportKeys, simulationKeys } from '../libs/queryKeys'; -import { useRegionsList } from './useStaticMetadata'; import { useHouseholdAssociationsByUser } from './useUserHousehold'; import { usePolicyAssociationsByUser } from './useUserPolicy'; import { useReportAssociationById, useReportAssociationsByUser } from './useUserReportAssociations'; @@ -69,9 +68,6 @@ export const useUserReports = (userId: string) => { const country = useCurrentCountry(); const queryNormalizer = useQueryNormalizer(); - // Get geography data from static metadata - const geographyOptions = useRegionsList(country); - // Step 1: Fetch all user associations in parallel const { data: reportAssociations, @@ -445,8 +441,6 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo ); // Step 7: Get geography data from simulations - const geographyOptions = useRegionsList(country); - const geographies: Geography[] = []; simulations.forEach((sim) => { if (sim.populationType === 'geography' && sim.populationId && sim.countryId) { diff --git a/app/src/hooks/useUserSimulations.ts b/app/src/hooks/useUserSimulations.ts index d2f5480b6..a7a4d2e15 100644 --- a/app/src/hooks/useUserSimulations.ts +++ b/app/src/hooks/useUserSimulations.ts @@ -14,7 +14,6 @@ import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation'; import { UserSimulation } from '@/types/ingredients/UserSimulation'; import { HouseholdMetadata } from '@/types/metadata/householdMetadata'; import { householdKeys, policyKeys, simulationKeys } from '../libs/queryKeys'; -import { useRegionsList } from './useStaticMetadata'; import { useHouseholdAssociationsByUser } from './useUserHousehold'; import { usePolicyAssociationsByUser } from './useUserPolicy'; import { useSimulationAssociationsByUser } from './useUserSimulationAssociations'; @@ -61,9 +60,6 @@ export const useUserSimulations = (userId: string) => { const country = useCurrentCountry(); const queryNormalizer = useQueryNormalizer(); - // Get geography data from static metadata - const geographyOptions = useRegionsList(country); - // Step 1: Fetch all user associations in parallel const { data: simulationAssociations, diff --git a/app/src/pages/Populations.page.tsx b/app/src/pages/Populations.page.tsx index 9c006f666..a6a6aab34 100644 --- a/app/src/pages/Populations.page.tsx +++ b/app/src/pages/Populations.page.tsx @@ -18,12 +18,7 @@ export default function PopulationsPage() { // Fetch household associations // Note: Geographic populations are no longer stored as user associations. // They are selected per-simulation and constructed from metadata. - const { - data: householdData, - isLoading, - isError, - error, - } = useUserHouseholds(userId); + const { data: householdData, isLoading, isError, error } = useUserHouseholds(userId); const navigate = useNavigate(); diff --git a/app/src/pages/Simulations.page.tsx b/app/src/pages/Simulations.page.tsx index 073cea508..b1555036a 100644 --- a/app/src/pages/Simulations.page.tsx +++ b/app/src/pages/Simulations.page.tsx @@ -11,7 +11,7 @@ import { useRegions } from '@/hooks/useRegions'; import { useUpdateSimulationAssociation } from '@/hooks/useUserSimulationAssociations'; import { useUserSimulations } from '@/hooks/useUserSimulations'; import { formatDate } from '@/utils/dateUtils'; -import { getRegionLabel, isNationalGeography, getCountryLabel } from '@/utils/geographyUtils'; +import { getCountryLabel, getRegionLabel, isNationalGeography } from '@/utils/geographyUtils'; export default function SimulationsPage() { const userId = MOCK_USER_ID.toString(); // TODO: Replace with actual user ID retrieval logic diff --git a/app/src/pages/report-output/GeographySubPage.tsx b/app/src/pages/report-output/GeographySubPage.tsx index 53bd0682f..4b7c10466 100644 --- a/app/src/pages/report-output/GeographySubPage.tsx +++ b/app/src/pages/report-output/GeographySubPage.tsx @@ -12,7 +12,10 @@ interface GeographySubPageProps { /** * Get display scope label from geography using V2 API metadata */ -function useGeographyDisplayInfo(geography: Geography | undefined, regions: ReturnType['regions']) { +function useGeographyDisplayInfo( + geography: Geography | undefined, + regions: ReturnType['regions'] +) { if (!geography) { return { label: '—', scopeLabel: '—' }; } diff --git a/app/src/pages/report-output/PopulationSubPage.tsx b/app/src/pages/report-output/PopulationSubPage.tsx index dcb8f1f2a..5be04438b 100644 --- a/app/src/pages/report-output/PopulationSubPage.tsx +++ b/app/src/pages/report-output/PopulationSubPage.tsx @@ -64,10 +64,7 @@ export default function PopulationSubPage({ const reformGeography = geographies?.find((g) => g.regionCode === reformRegionCode); return ( - + ); } diff --git a/app/src/pathways/population/PopulationPathwayWrapper.tsx b/app/src/pathways/population/PopulationPathwayWrapper.tsx index 23e104476..e948d8f37 100644 --- a/app/src/pathways/population/PopulationPathwayWrapper.tsx +++ b/app/src/pathways/population/PopulationPathwayWrapper.tsx @@ -11,10 +11,10 @@ import StandardLayout from '@/components/StandardLayout'; import { CURRENT_YEAR } from '@/constants'; import { ReportYearProvider } from '@/contexts/ReportYearContext'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { usePathwayNavigation } from '@/hooks/usePathwayNavigation'; import { Household } from '@/types/ingredients/Household'; import { StandalonePopulationViewMode } from '@/types/pathwayModes/PopulationViewMode'; import { PopulationStateProps } from '@/types/pathwayState'; -import { usePathwayNavigation } from '@/hooks/usePathwayNavigation'; import HouseholdBuilderView from '../report/views/population/HouseholdBuilderView'; import PopulationLabelView from '../report/views/population/PopulationLabelView'; diff --git a/app/src/pathways/report/ReportPathwayWrapper.tsx b/app/src/pathways/report/ReportPathwayWrapper.tsx index e030b8a82..575143762 100644 --- a/app/src/pathways/report/ReportPathwayWrapper.tsx +++ b/app/src/pathways/report/ReportPathwayWrapper.tsx @@ -8,7 +8,6 @@ */ import { useCallback, useState } from 'react'; -import { useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; import { ReportAdapter } from '@/adapters'; import StandardLayout from '@/components/StandardLayout'; @@ -20,7 +19,6 @@ import { useCurrentLawId, useRegionsList } from '@/hooks/useStaticMetadata'; import { useUserHouseholds } from '@/hooks/useUserHousehold'; import { useUserSimulations } from '@/hooks/useUserSimulations'; import { countryIds } from '@/libs/countries'; -import { RootState } from '@/store'; import { Report } from '@/types/ingredients/Report'; import { ReportViewMode } from '@/types/pathwayModes/ReportViewMode'; import { ReportStateProps, SimulationStateProps } from '@/types/pathwayState'; @@ -88,8 +86,6 @@ export default function ReportPathwayWrapper({ onComplete }: ReportPathwayWrappe const { createReport, isPending: isSubmitting } = useCreateReport(reportState.label || undefined); - // Get metadata for population views - const metadata = useSelector((state: RootState) => state.metadata); const currentLawId = useCurrentLawId(countryId); const regionData = useRegionsList(countryId); diff --git a/app/src/pathways/report/views/ReportSetupView.tsx b/app/src/pathways/report/views/ReportSetupView.tsx index a64be18ea..cf452c6e1 100644 --- a/app/src/pathways/report/views/ReportSetupView.tsx +++ b/app/src/pathways/report/views/ReportSetupView.tsx @@ -4,8 +4,8 @@ import { MOCK_USER_ID } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { useRegions } from '@/hooks/useRegions'; import { useUserHouseholds } from '@/hooks/useUserHousehold'; -import { ReportStateProps, SimulationStateProps } from '@/types/pathwayState'; import { MetadataRegionEntry } from '@/types/metadata'; +import { ReportStateProps, SimulationStateProps } from '@/types/pathwayState'; import { getCountryLabel, getRegionLabel, isNationalGeography } from '@/utils/geographyUtils'; import { isSimulationConfigured } from '@/utils/validation/ingredientValidation'; diff --git a/app/src/pathways/report/views/ReportSimulationExistingView.tsx b/app/src/pathways/report/views/ReportSimulationExistingView.tsx index c9b43fc76..1c4baaaf7 100644 --- a/app/src/pathways/report/views/ReportSimulationExistingView.tsx +++ b/app/src/pathways/report/views/ReportSimulationExistingView.tsx @@ -35,7 +35,9 @@ export default function ReportSimulationExistingView({ // Helper to get geography display label in "Households in {label}" format function getGeographyLabel(enhancedSim: EnhancedUserSimulation): string | undefined { - if (!enhancedSim.geography) return undefined; + if (!enhancedSim.geography) { + return undefined; + } const label = isNationalGeography(enhancedSim.geography) ? getCountryLabel(enhancedSim.geography.countryId) : getRegionLabel(enhancedSim.geography.regionCode, regions); @@ -111,8 +113,7 @@ export default function ReportSimulationExistingView({ // For household populations, use household.id // For geography populations, use geography.regionCode const otherPopulationId = - otherSimulation?.population.household?.id || - otherSimulation?.population.geography?.regionCode; + otherSimulation?.population.household?.id || otherSimulation?.population.geography?.regionCode; // Sort simulations to show compatible first, then incompatible const sortedSimulations = [...filteredSimulations].sort((a, b) => { diff --git a/app/src/pathways/report/views/ReportSubmitView.tsx b/app/src/pathways/report/views/ReportSubmitView.tsx index aa18a1087..0deb0db0d 100644 --- a/app/src/pathways/report/views/ReportSubmitView.tsx +++ b/app/src/pathways/report/views/ReportSubmitView.tsx @@ -31,8 +31,12 @@ export default function ReportSubmitView({ // Get population label - use label if available, otherwise fall back to ID const populationLabel = simulation.population.label || - (simulation.population.household?.id ? `Household #${simulation.population.household.id}` : null) || - (simulation.population.geography?.regionCode ? `Households in ${simulation.population.geography.regionCode}` : null) || + (simulation.population.household?.id + ? `Household #${simulation.population.household.id}` + : null) || + (simulation.population.geography?.regionCode + ? `Households in ${simulation.population.geography.regionCode}` + : null) || 'No population'; return `${policyLabel} • ${populationLabel}`; diff --git a/app/src/pathways/report/views/population/HouseholdBuilderView.tsx b/app/src/pathways/report/views/population/HouseholdBuilderView.tsx index c1641af3a..02697a0d4 100644 --- a/app/src/pathways/report/views/population/HouseholdBuilderView.tsx +++ b/app/src/pathways/report/views/population/HouseholdBuilderView.tsx @@ -43,10 +43,7 @@ export default function HouseholdBuilderView({ const { loading, error } = reduxMetadata; // Merge static entities into metadata so VariableResolver can resolve entity types - const metadata = useMemo( - () => ({ ...reduxMetadata, entities }), - [reduxMetadata, entities] - ); + const metadata = useMemo(() => ({ ...reduxMetadata, entities }), [reduxMetadata, entities]); // Get all basic non-person fields dynamically (country-agnostic) // This handles US entities (tax_unit, spm_unit, etc.) and UK entities (benunit) automatically diff --git a/app/src/pathways/report/views/population/PopulationExistingView.tsx b/app/src/pathways/report/views/population/PopulationExistingView.tsx index ea03a1719..a9265c8de 100644 --- a/app/src/pathways/report/views/population/PopulationExistingView.tsx +++ b/app/src/pathways/report/views/population/PopulationExistingView.tsx @@ -38,12 +38,7 @@ export default function PopulationExistingView({ // Fetch household populations only // Geographic populations are no longer stored as user associations - const { - data: householdData, - isLoading, - isError, - error, - } = useUserHouseholds(userId); + const { data: householdData, isLoading, isError, error } = useUserHouseholds(userId); const [localPopulation, setLocalPopulation] = useState(null); diff --git a/app/src/pathways/report/views/population/PopulationScopeView.tsx b/app/src/pathways/report/views/population/PopulationScopeView.tsx index cf3c18f1a..b76acec83 100644 --- a/app/src/pathways/report/views/population/PopulationScopeView.tsx +++ b/app/src/pathways/report/views/population/PopulationScopeView.tsx @@ -33,7 +33,11 @@ import USGeographicOptions from '../../components/geographicOptions/USGeographic interface PopulationScopeViewProps { countryId: (typeof countryIds)[number]; regionData: any[]; - onScopeSelected: (geography: Geography | null, scopeType: ScopeType, regionLabel?: string) => void; + onScopeSelected: ( + geography: Geography | null, + scopeType: ScopeType, + regionLabel?: string + ) => void; onBack?: () => void; onCancel?: () => void; } @@ -70,10 +74,18 @@ export default function PopulationScopeView({ } // For subnational scopes, find the region in the appropriate list - if (!selectedRegion) return ''; + if (!selectedRegion) { + return ''; + } // Check all region option lists for the selected value - const allOptions = [...usStates, ...usDistricts, ...ukCountries, ...ukConstituencies, ...ukLocalAuthorities]; + const allOptions = [ + ...usStates, + ...usDistricts, + ...ukCountries, + ...ukConstituencies, + ...ukLocalAuthorities, + ]; const matchedRegion = allOptions.find((r) => r.value === selectedRegion); return matchedRegion?.label || ''; } diff --git a/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx b/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx index e7a147575..c119ed901 100644 --- a/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx +++ b/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx @@ -61,8 +61,12 @@ export default function SimulationPopulationSetupView({ ); function getOtherPopulationLabel(): string { - if (otherPopulation?.label) return otherPopulation.label; - if (otherPopulation?.household?.id) return `Household #${otherPopulation.household.id}`; + if (otherPopulation?.label) { + return otherPopulation.label; + } + if (otherPopulation?.household?.id) { + return `Household #${otherPopulation.household.id}`; + } if (otherPopulation?.geography) { const geo = otherPopulation.geography; const label = isNationalGeography(geo) diff --git a/app/src/pathways/report/views/simulation/SimulationSetupView.tsx b/app/src/pathways/report/views/simulation/SimulationSetupView.tsx index 256c26582..9931f92e1 100644 --- a/app/src/pathways/report/views/simulation/SimulationSetupView.tsx +++ b/app/src/pathways/report/views/simulation/SimulationSetupView.tsx @@ -71,7 +71,9 @@ export default function SimulationSetupView({ // Helper to get geography display label in "Households in {label}" format function getGeographyDisplayLabel(): string { - if (!population.geography) return ''; + if (!population.geography) { + return ''; + } const label = isNationalGeography(population.geography) ? getCountryLabel(population.geography.countryId) : getRegionLabel(population.geography.regionCode, regions); diff --git a/app/src/pathways/report/views/simulation/SimulationSubmitView.tsx b/app/src/pathways/report/views/simulation/SimulationSubmitView.tsx index 7b8866af3..1bcfb19df 100644 --- a/app/src/pathways/report/views/simulation/SimulationSubmitView.tsx +++ b/app/src/pathways/report/views/simulation/SimulationSubmitView.tsx @@ -57,11 +57,17 @@ export default function SimulationSubmitView({ } // Create summary boxes based on the current simulation state - const populationIdentifier = simulation.population.household?.id || simulation.population.geography?.regionCode; - const populationDisplay = simulation.population.label - || (simulation.population.household?.id ? `Household #${simulation.population.household.id}` : null) - || (simulation.population.geography?.regionCode ? `Households in ${simulation.population.geography.regionCode}` : null) - || 'No population'; + const populationIdentifier = + simulation.population.household?.id || simulation.population.geography?.regionCode; + const populationDisplay = + simulation.population.label || + (simulation.population.household?.id + ? `Household #${simulation.population.household.id}` + : null) || + (simulation.population.geography?.regionCode + ? `Households in ${simulation.population.geography.regionCode}` + : null) || + 'No population'; const summaryBoxes: SummaryBoxItem[] = [ { title: 'Population added', diff --git a/app/src/pathways/simulation/SimulationPathwayWrapper.tsx b/app/src/pathways/simulation/SimulationPathwayWrapper.tsx index 5d5416f49..28354a5f2 100644 --- a/app/src/pathways/simulation/SimulationPathwayWrapper.tsx +++ b/app/src/pathways/simulation/SimulationPathwayWrapper.tsx @@ -6,7 +6,6 @@ */ import { useCallback, useState } from 'react'; -import { useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import StandardLayout from '@/components/StandardLayout'; import { MOCK_USER_ID } from '@/constants'; @@ -15,7 +14,6 @@ import { usePathwayNavigation } from '@/hooks/usePathwayNavigation'; import { useCurrentLawId, useRegionsList } from '@/hooks/useStaticMetadata'; import { useUserHouseholds } from '@/hooks/useUserHousehold'; import { useUserPolicies } from '@/hooks/useUserPolicy'; -import { RootState } from '@/store'; import { SimulationViewMode } from '@/types/pathwayModes/SimulationViewMode'; import { SimulationStateProps } from '@/types/pathwayState'; import { diff --git a/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx b/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx index d0ba99221..3d49a0c58 100644 --- a/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx +++ b/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx @@ -9,13 +9,9 @@ import LazyNestedMenu from '@/components/common/LazyNestedMenu'; import { createMockGetChildren, createMockOnParameterClick, - MOCK_BENEFIT_CHILDREN, MOCK_EMPTY_NODES, MOCK_LEAF_NODES, MOCK_ROOT_NODES, - MOCK_SINGLE_BRANCH_NODE, - MOCK_SINGLE_LEAF_NODE, - MOCK_TAX_CHILDREN, TEST_NODE_LABELS, TEST_NODE_NAMES, } from '@/tests/fixtures/components/LazyNestedMenuMocks'; diff --git a/app/src/tests/unit/hooks/useLazyParameterTree.test.ts b/app/src/tests/unit/hooks/useLazyParameterTree.test.ts index c71a151b8..e4d7a716a 100644 --- a/app/src/tests/unit/hooks/useLazyParameterTree.test.ts +++ b/app/src/tests/unit/hooks/useLazyParameterTree.test.ts @@ -9,13 +9,10 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { useLazyParameterTree } from '@/hooks/useLazyParameterTree'; import { createMockRootState, - EXPECTED_GOV_CHILDREN, - EXPECTED_TAX_CHILDREN, MOCK_METADATA_EMPTY, MOCK_METADATA_ERROR, MOCK_METADATA_LOADED, MOCK_METADATA_LOADING, - MOCK_PARAMETERS_FOR_HOOK, TEST_ERROR_MESSAGE, } from '@/tests/fixtures/hooks/useLazyParameterTreeMocks'; diff --git a/app/src/tests/unit/hooks/useStaticMetadata.test.ts b/app/src/tests/unit/hooks/useStaticMetadata.test.ts index 94e5f6ff5..8269ef120 100644 --- a/app/src/tests/unit/hooks/useStaticMetadata.test.ts +++ b/app/src/tests/unit/hooks/useStaticMetadata.test.ts @@ -8,7 +8,7 @@ import { useStaticMetadata, useTimePeriods, } from '@/hooks/useStaticMetadata'; -import { TEST_COUNTRIES, TEST_YEAR } from '@/tests/fixtures/hooks/metadataHooksMocks'; +import { TEST_COUNTRIES } from '@/tests/fixtures/hooks/metadataHooksMocks'; describe('useStaticMetadata', () => { describe('useStaticMetadata (composite hook)', () => { diff --git a/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx b/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx index 5aaa5c907..86b8d4821 100644 --- a/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx +++ b/app/src/tests/unit/pathways/population/PopulationPathwayWrapper.test.tsx @@ -5,9 +5,9 @@ import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import PopulationPathwayWrapper from '@/pathways/population/PopulationPathwayWrapper'; import { mockNavigate, + mockUseParams, mockUsePathwayNavigationReturn, mockUseRegionsEmpty, - mockUseParams, resetAllMocks, TEST_COUNTRY_ID, } from '@/tests/fixtures/pathways/population/PopulationPathwayWrapperMocks'; diff --git a/app/src/types/ingredients/index.ts b/app/src/types/ingredients/index.ts index 14db78369..8bdeff9d1 100644 --- a/app/src/types/ingredients/index.ts +++ b/app/src/types/ingredients/index.ts @@ -87,10 +87,4 @@ export function isUserReport(obj: UserIngredient): obj is UserReport { // Export all types export type { Geography, Household, Policy, Population, Report, Simulation }; -export type { - UserPolicy, - UserHouseholdPopulation, - UserPopulation, - UserReport, - UserSimulation, -}; +export type { UserPolicy, UserHouseholdPopulation, UserPopulation, UserReport, UserSimulation }; diff --git a/app/src/utils/geographyUtils.ts b/app/src/utils/geographyUtils.ts index 3754eaf04..46070eb0c 100644 --- a/app/src/utils/geographyUtils.ts +++ b/app/src/utils/geographyUtils.ts @@ -1,5 +1,4 @@ -import type { Geography } from '@/types/ingredients/Geography'; -import { isNationalGeography } from '@/types/ingredients/Geography'; +import { isNationalGeography, type Geography } from '@/types/ingredients/Geography'; import { MetadataRegionEntry } from '@/types/metadata'; import { UK_REGION_TYPES } from '@/types/regionTypes'; diff --git a/app/src/utils/pathwayCallbacks/populationCallbacks.ts b/app/src/utils/pathwayCallbacks/populationCallbacks.ts index 568f135bf..dcfd48d23 100644 --- a/app/src/utils/pathwayCallbacks/populationCallbacks.ts +++ b/app/src/utils/pathwayCallbacks/populationCallbacks.ts @@ -72,7 +72,15 @@ export function createPopulationCallbacks( navigateToMode(labelMode); } }, - [setState, populationSelector, populationUpdater, navigateToMode, labelMode, returnMode, onPopulationComplete] + [ + setState, + populationSelector, + populationUpdater, + navigateToMode, + labelMode, + returnMode, + onPopulationComplete, + ] ); const handleSelectExistingHousehold = useCallback( diff --git a/app/src/utils/validation/ingredientValidation.ts b/app/src/utils/validation/ingredientValidation.ts index 0ae8e85e5..4ad6fe350 100644 --- a/app/src/utils/validation/ingredientValidation.ts +++ b/app/src/utils/validation/ingredientValidation.ts @@ -130,4 +130,3 @@ export function isHouseholdAssociationReady( return true; } - From 5a3b3ecfc7a7200500349fcac92665764cd21d83 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Tue, 17 Feb 2026 22:58:31 +0100 Subject: [PATCH 16/17] fix: Add missing countryId to UserPolicy fixtures and format files After rebasing onto move-to-api-v2 (which includes the policy v2 migration), countryId became required on UserPolicy and ShareableUserPolicy. Updated all fixtures and useSaveSharedReport to include countryId. Also ran Prettier on files that needed formatting after the rebase. Co-Authored-By: Claude Opus 4.6 --- app/src/api/policyAssociation.ts | 10 ++++++---- app/src/api/v2/taxBenefitModels.ts | 3 +-- app/src/api/v2/userPolicyAssociations.ts | 1 - app/src/hooks/useSaveSharedReport.ts | 1 + app/src/hooks/useUserId.ts | 1 - .../tests/fixtures/api/associationFixtures.ts | 1 + app/src/tests/fixtures/api/policyMocks.ts | 4 +++- .../hooks/useFetchReportIngredientsMocks.ts | 6 +++--- .../fixtures/hooks/useSaveSharedReportMocks.ts | 6 +++--- .../hooks/useSharedReportDataMocks.tsx | 4 ++-- app/src/tests/fixtures/pages/policiesMocks.ts | 2 ++ .../pages/report-output/PolicySubPage.ts | 2 ++ .../report-output/SocietyWideReportOutput.ts | 1 + .../fixtures/utils/policyColumnHeaders.ts | 2 ++ .../tests/unit/api/policyAssociation.test.ts | 18 +++++++++++------- 15 files changed, 38 insertions(+), 24 deletions(-) diff --git a/app/src/api/policyAssociation.ts b/app/src/api/policyAssociation.ts index af854b568..4b65a9fac 100644 --- a/app/src/api/policyAssociation.ts +++ b/app/src/api/policyAssociation.ts @@ -11,7 +11,11 @@ export interface UserPolicyStore { create: (policy: Omit) => Promise; findByUser: (userId: string, countryId?: string) => Promise; findById: (userPolicyId: string) => Promise; - update: (userPolicyId: string, updates: Partial, userId: string) => Promise; + update: ( + userPolicyId: string, + updates: Partial, + userId: string + ) => Promise; delete: (userPolicyId: string, userId: string) => Promise; } @@ -71,9 +75,7 @@ export class LocalStoragePolicyStore implements UserPolicyStore { async findByUser(userId: string, countryId?: string): Promise { const policies = this.getStoredPolicies(); - return policies.filter( - (p) => p.userId === userId && (!countryId || p.countryId === countryId) - ); + return policies.filter((p) => p.userId === userId && (!countryId || p.countryId === countryId)); } async findById(userPolicyId: string): Promise { diff --git a/app/src/api/v2/taxBenefitModels.ts b/app/src/api/v2/taxBenefitModels.ts index c84c18418..81204f0c4 100644 --- a/app/src/api/v2/taxBenefitModels.ts +++ b/app/src/api/v2/taxBenefitModels.ts @@ -1,5 +1,4 @@ -export const API_V2_BASE_URL = - import.meta.env.VITE_API_V2_URL || 'https://v2.api.policyengine.org'; +export const API_V2_BASE_URL = import.meta.env.VITE_API_V2_URL || 'https://v2.api.policyengine.org'; /** * Map country IDs to their API model names. diff --git a/app/src/api/v2/userPolicyAssociations.ts b/app/src/api/v2/userPolicyAssociations.ts index 45ca1f771..687f4710d 100644 --- a/app/src/api/v2/userPolicyAssociations.ts +++ b/app/src/api/v2/userPolicyAssociations.ts @@ -14,7 +14,6 @@ import { CountryId } from '@/libs/countries'; import { UserPolicy } from '@/types/ingredients/UserPolicy'; - import { API_V2_BASE_URL } from './taxBenefitModels'; // ============================================================================ diff --git a/app/src/hooks/useSaveSharedReport.ts b/app/src/hooks/useSaveSharedReport.ts index 3159519f9..333592b70 100644 --- a/app/src/hooks/useSaveSharedReport.ts +++ b/app/src/hooks/useSaveSharedReport.ts @@ -82,6 +82,7 @@ export function useSaveSharedReport() { createPolicyAssociation.mutateAsync({ userId, policyId: policy.policyId, + countryId: policy.countryId, label: policy.label ?? undefined, }) ); diff --git a/app/src/hooks/useUserId.ts b/app/src/hooks/useUserId.ts index 2e222eb47..bbce9562d 100644 --- a/app/src/hooks/useUserId.ts +++ b/app/src/hooks/useUserId.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react'; - import { getUserId } from '@/libs/userIdentity'; /** diff --git a/app/src/tests/fixtures/api/associationFixtures.ts b/app/src/tests/fixtures/api/associationFixtures.ts index ded9b9fcf..17fbe96ee 100644 --- a/app/src/tests/fixtures/api/associationFixtures.ts +++ b/app/src/tests/fixtures/api/associationFixtures.ts @@ -63,6 +63,7 @@ export const createMockPolicyAssociation = (overrides?: Partial): Us id: TEST_IDS.USER_POLICY_ID, userId: TEST_IDS.USER_ID, policyId: TEST_IDS.POLICY_ID, + countryId: 'us', label: TEST_LABELS.POLICY, createdAt: TEST_TIMESTAMPS.CREATED_AT, isCreated: true, diff --git a/app/src/tests/fixtures/api/policyMocks.ts b/app/src/tests/fixtures/api/policyMocks.ts index 0aaedfb6a..20680a1d9 100644 --- a/app/src/tests/fixtures/api/policyMocks.ts +++ b/app/src/tests/fixtures/api/policyMocks.ts @@ -30,7 +30,9 @@ export const mockV2PolicyResponse = (overrides?: Partial): V2P /** * V2 Policy creation payload mock */ -export const mockV2PolicyPayload = (overrides?: Partial): V2PolicyCreatePayload => ({ +export const mockV2PolicyPayload = ( + overrides?: Partial +): V2PolicyCreatePayload => ({ name: 'New Policy', tax_benefit_model_id: TEST_TAX_BENEFIT_MODEL_ID, parameter_values: [], diff --git a/app/src/tests/fixtures/hooks/useFetchReportIngredientsMocks.ts b/app/src/tests/fixtures/hooks/useFetchReportIngredientsMocks.ts index c18ffb613..dabdc1d34 100644 --- a/app/src/tests/fixtures/hooks/useFetchReportIngredientsMocks.ts +++ b/app/src/tests/fixtures/hooks/useFetchReportIngredientsMocks.ts @@ -68,8 +68,8 @@ export const SOCIETY_WIDE_INPUT: ReportIngredientsInput = { { simulationId: TEST_IDS.SIMULATIONS.REFORM, countryId: TEST_COUNTRIES.US, label: 'Reform' }, ], userPolicies: [ - { policyId: TEST_IDS.POLICIES.CURRENT_LAW, label: 'Current Law' }, - { policyId: TEST_IDS.POLICIES.REFORM, label: 'My Reform' }, + { policyId: TEST_IDS.POLICIES.CURRENT_LAW, countryId: TEST_COUNTRIES.US, label: 'Current Law' }, + { policyId: TEST_IDS.POLICIES.REFORM, countryId: TEST_COUNTRIES.US, label: 'My Reform' }, ], userHouseholds: [], }; @@ -87,7 +87,7 @@ export const HOUSEHOLD_INPUT: ReportIngredientsInput = { userSimulations: [ { simulationId: 'sim-hh-1', countryId: TEST_COUNTRIES.UK, label: 'Household Sim' }, ], - userPolicies: [{ policyId: 'policy-hh-1', label: 'HH Policy' }], + userPolicies: [{ policyId: 'policy-hh-1', countryId: TEST_COUNTRIES.UK, label: 'HH Policy' }], userHouseholds: [ { type: 'household', diff --git a/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts b/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts index 82737af06..c2edb274c 100644 --- a/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts +++ b/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts @@ -44,15 +44,15 @@ export const MOCK_SAVE_SHARE_DATA: ReportIngredientsInput = { userSimulations: [ { simulationId: TEST_IDS.SIMULATION, countryId: TEST_COUNTRIES.US, label: 'Baseline' }, ], - userPolicies: [{ policyId: TEST_IDS.POLICY, label: 'My Policy' }], + userPolicies: [{ policyId: TEST_IDS.POLICY, countryId: TEST_COUNTRIES.US, label: 'My Policy' }], userHouseholds: [], }; export const MOCK_SHARE_DATA_WITH_CURRENT_LAW: ReportIngredientsInput = { ...MOCK_SAVE_SHARE_DATA, userPolicies: [ - { policyId: TEST_IDS.CURRENT_LAW_POLICY, label: 'Current Law' }, - { policyId: TEST_IDS.POLICY, label: 'My Policy' }, + { policyId: TEST_IDS.CURRENT_LAW_POLICY, countryId: TEST_COUNTRIES.US, label: 'Current Law' }, + { policyId: TEST_IDS.POLICY, countryId: TEST_COUNTRIES.US, label: 'My Policy' }, ], }; diff --git a/app/src/tests/fixtures/hooks/useSharedReportDataMocks.tsx b/app/src/tests/fixtures/hooks/useSharedReportDataMocks.tsx index 71b0bd3d5..4c9844929 100644 --- a/app/src/tests/fixtures/hooks/useSharedReportDataMocks.tsx +++ b/app/src/tests/fixtures/hooks/useSharedReportDataMocks.tsx @@ -18,7 +18,7 @@ export const MOCK_SHARE_DATA: ReportIngredientsInput = { label: 'Test Report', }, userSimulations: [{ simulationId: 'sim-1', countryId: 'us', label: 'Baseline Sim' }], - userPolicies: [{ policyId: 'policy-1', label: 'Test Policy' }], + userPolicies: [{ policyId: 'policy-1', countryId: 'us', label: 'Test Policy' }], userHouseholds: [], }; @@ -30,7 +30,7 @@ export const MOCK_HOUSEHOLD_SHARE_DATA: ReportIngredientsInput = { label: 'Household Report', }, userSimulations: [{ simulationId: 'sim-2', countryId: 'uk', label: 'HH Sim' }], - userPolicies: [{ policyId: 'policy-2', label: 'HH Policy' }], + userPolicies: [{ policyId: 'policy-2', countryId: 'uk', label: 'HH Policy' }], userHouseholds: [ { type: 'household', householdId: 'hh-1', countryId: 'uk', label: 'My Household' }, ], diff --git a/app/src/tests/fixtures/pages/policiesMocks.ts b/app/src/tests/fixtures/pages/policiesMocks.ts index 4519ed63e..c3ad57435 100644 --- a/app/src/tests/fixtures/pages/policiesMocks.ts +++ b/app/src/tests/fixtures/pages/policiesMocks.ts @@ -29,6 +29,7 @@ export const mockPolicyData: UserPolicyWithAssociation[] = [ id: 'assoc-1', userId: '1', policyId: '101', + countryId: 'us', label: 'Test Policy 1', createdAt: '2024-01-15T10:00:00Z', }, @@ -59,6 +60,7 @@ export const mockPolicyData: UserPolicyWithAssociation[] = [ id: 'assoc-2', userId: '1', policyId: '102', + countryId: 'us', label: 'Test Policy 2', createdAt: '2024-02-20T14:30:00Z', }, diff --git a/app/src/tests/fixtures/pages/report-output/PolicySubPage.ts b/app/src/tests/fixtures/pages/report-output/PolicySubPage.ts index c88f782a2..3746215cc 100644 --- a/app/src/tests/fixtures/pages/report-output/PolicySubPage.ts +++ b/app/src/tests/fixtures/pages/report-output/PolicySubPage.ts @@ -153,6 +153,7 @@ export const mockUserBaselinePolicy: UserPolicy = { id: 'user-pol-baseline-123', userId: TEST_USER_ID, policyId: TEST_POLICY_IDS.BASELINE, + countryId: 'us', label: 'My Baseline Policy', createdAt: '2025-01-15T10:00:00Z', }; @@ -161,6 +162,7 @@ export const mockUserReformPolicy: UserPolicy = { id: 'user-pol-reform-456', userId: TEST_USER_ID, policyId: TEST_POLICY_IDS.REFORM, + countryId: 'us', label: 'My Reform Policy', createdAt: '2025-01-15T11:00:00Z', }; diff --git a/app/src/tests/fixtures/pages/report-output/SocietyWideReportOutput.ts b/app/src/tests/fixtures/pages/report-output/SocietyWideReportOutput.ts index fc785c768..27c2eb470 100644 --- a/app/src/tests/fixtures/pages/report-output/SocietyWideReportOutput.ts +++ b/app/src/tests/fixtures/pages/report-output/SocietyWideReportOutput.ts @@ -71,6 +71,7 @@ export const MOCK_USER_POLICY: UserPolicy = { id: 'user-policy-1', userId: 'user-123', policyId: 'policy-1', + countryId: 'us', label: 'My Policy', createdAt: '2024-01-01T00:00:00Z', }; diff --git a/app/src/tests/fixtures/utils/policyColumnHeaders.ts b/app/src/tests/fixtures/utils/policyColumnHeaders.ts index 31fe8a7a4..5391b9a37 100644 --- a/app/src/tests/fixtures/utils/policyColumnHeaders.ts +++ b/app/src/tests/fixtures/utils/policyColumnHeaders.ts @@ -22,6 +22,7 @@ export const MOCK_USER_POLICY_BASELINE: UserPolicy = { id: 'user-policy-1', userId: 'user-123', policyId: 'policy-1', + countryId: 'us', label: 'My Baseline', createdAt: '2024-01-01T00:00:00Z', }; @@ -30,6 +31,7 @@ export const MOCK_USER_POLICY_REFORM: UserPolicy = { id: 'user-policy-2', userId: 'user-123', policyId: 'policy-2', + countryId: 'us', label: 'My Reform', createdAt: '2024-01-01T00:00:00Z', }; diff --git a/app/src/tests/unit/api/policyAssociation.test.ts b/app/src/tests/unit/api/policyAssociation.test.ts index 9b670bb4a..dd97c1041 100644 --- a/app/src/tests/unit/api/policyAssociation.test.ts +++ b/app/src/tests/unit/api/policyAssociation.test.ts @@ -208,7 +208,11 @@ describe('ApiPolicyStore', () => { }); // When - const result = await store.update('user-policy-abc123', { label: 'Updated Label' }, 'user-123'); + const result = await store.update( + 'user-policy-abc123', + { label: 'Updated Label' }, + 'user-123' + ); // Then expect(fetch).toHaveBeenCalledWith( @@ -230,9 +234,9 @@ describe('ApiPolicyStore', () => { }); // When/Then - await expect(store.update('user-policy-abc123', { label: 'Updated Label' }, 'user-123')).rejects.toThrow( - 'Failed to update policy association' - ); + await expect( + store.update('user-policy-abc123', { label: 'Updated Label' }, 'user-123') + ).rejects.toThrow('Failed to update policy association'); }); }); @@ -445,9 +449,9 @@ describe('LocalStoragePolicyStore', () => { // Given - no policy created // When & Then - await expect(store.update('sup-nonexistent', { label: 'Updated Label' }, 'user-123')).rejects.toThrow( - 'UserPolicy with id sup-nonexistent not found' - ); + await expect( + store.update('sup-nonexistent', { label: 'Updated Label' }, 'user-123') + ).rejects.toThrow('UserPolicy with id sup-nonexistent not found'); }); it('given existing policy then updatedAt timestamp is set', async () => { From 26080aba92a3cf35750c623b5e9515ec4b0ddbc1 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Tue, 17 Feb 2026 23:03:01 +0100 Subject: [PATCH 17/17] fix: Mock useUserId in Policies.page test The page now uses useUserId() (from the policy v2 migration) instead of MOCK_USER_ID directly. Added mock to return MOCK_USER_ID so the test assertion matches. Co-Authored-By: Claude Opus 4.6 --- app/src/tests/unit/pages/Policies.page.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/tests/unit/pages/Policies.page.test.tsx b/app/src/tests/unit/pages/Policies.page.test.tsx index f0d70507d..3b23fcaab 100644 --- a/app/src/tests/unit/pages/Policies.page.test.tsx +++ b/app/src/tests/unit/pages/Policies.page.test.tsx @@ -28,6 +28,11 @@ vi.mock('@/hooks/useCurrentCountry', () => ({ useCurrentCountry: () => 'us', })); +// Mock useUserId to return MOCK_USER_ID +vi.mock('@/hooks/useUserId', () => ({ + useUserId: () => MOCK_USER_ID.toString(), +})); + // Mock useNavigate const mockNavigate = vi.fn(); vi.mock('react-router-dom', async () => {