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: 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/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/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 b6c1554cc..4e38a3135 100644 --- a/app/src/adapters/index.ts +++ b/app/src/adapters/index.ts @@ -5,12 +5,12 @@ 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'; 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/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/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/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/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/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/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/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/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/useSaveSharedReport.ts b/app/src/hooks/useSaveSharedReport.ts index cc13210ed..333592b70 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 @@ -84,6 +82,7 @@ export function useSaveSharedReport() { createPolicyAssociation.mutateAsync({ userId, policyId: policy.policyId, + countryId: policy.countryId, label: policy.label ?? undefined, }) ); @@ -98,23 +97,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 +148,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/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/hooks/useUserGeographic.ts b/app/src/hooks/useUserGeographic.ts deleted file mode 100644 index 1b387aaee..000000000 --- a/app/src/hooks/useUserGeographic.ts +++ /dev/null @@ -1,215 +0,0 @@ -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'; -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 currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); - - // 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/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/hooks/useUserReports.ts b/app/src/hooks/useUserReports.ts index 74e48f804..883f7c768 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'; @@ -13,15 +12,10 @@ 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'; @@ -53,7 +47,6 @@ export interface EnhancedUserReport { userSimulations?: UserSimulation[]; userPolicies?: UserPolicy[]; userHouseholds?: UserHouseholdPopulation[]; - userGeographies?: UserGeographyPopulation[]; // Status isLoading: boolean; @@ -75,10 +68,6 @@ export const useUserReports = (userId: string) => { const country = useCurrentCountry(); const queryNormalizer = useQueryNormalizer(); - // Get geography data from static metadata - const currentYear = parseInt(CURRENT_YEAR, 10); - const geographyOptions = useRegionsList(country, currentYear); - // Step 1: Fetch all user associations in parallel const { data: reportAssociations, @@ -240,17 +229,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); } } }); @@ -347,7 +330,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 { @@ -428,7 +410,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) @@ -460,46 +441,19 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo ); // Step 7: Get geography data from simulations - const geographyOptions = useRegionsList(country, currentYear); - 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); } }); - // 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, @@ -510,7 +464,6 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo userSimulations, userPolicies, userHouseholds, - userGeographies, isLoading: userReportLoading || repLoading || diff --git a/app/src/hooks/useUserSimulations.ts b/app/src/hooks/useUserSimulations.ts index 92ba00e63..a7a4d2e15 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'; @@ -15,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'; @@ -62,10 +60,6 @@ export const useUserSimulations = (userId: string) => { const country = useCurrentCountry(); const queryNormalizer = useQueryNormalizer(); - // Get geography data from static metadata - const currentYear = parseInt(CURRENT_YEAR, 10); - const geographyOptions = useRegionsList(country, currentYear); - // Step 1: Fetch all user associations in parallel const { data: simulationAssociations, @@ -179,18 +173,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 9f5636e4e..c502d53d6 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'; @@ -26,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'; @@ -40,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 via useRegions(). * * @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, }); } }); @@ -97,10 +78,6 @@ export type ShareableUserHousehold = Omit< UserHouseholdPopulation, 'userId' | 'createdAt' | 'updatedAt' >; -export type ShareableUserGeography = Omit< - UserGeographyPopulation, - 'userId' | 'createdAt' | 'updatedAt' ->; /** * Input for useFetchReportIngredients @@ -111,7 +88,6 @@ export interface ReportIngredientsInput { userSimulations: ShareableUserSimulation[]; userPolicies: ShareableUserPolicy[]; userHouseholds: ShareableUserHousehold[]; - userGeographies: ShareableUserGeography[]; } /** @@ -140,7 +116,6 @@ export function expandUserAssociations( userSimulations: UserSimulation[]; userPolicies: UserPolicy[]; userHouseholds: UserHouseholdPopulation[]; - userGeographies: UserGeographyPopulation[]; } { return { userReport: { @@ -160,10 +135,6 @@ export function expandUserAssociations( ...h, userId, })), - userGeographies: input.userGeographies.map((g) => ({ - ...g, - userId, - })), }; } @@ -187,8 +158,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/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/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/libs/queryKeys.ts b/app/src/libs/queryKeys.ts index 88f9afbe4..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, @@ -111,3 +99,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..a6a6aab34 100644 --- a/app/src/pages/Populations.page.tsx +++ b/app/src/pages/Populations.page.tsx @@ -5,42 +5,20 @@ 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 { - 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 currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); // Fetch household associations - const { - data: householdData, - isLoading: isHouseholdLoading, - isError: isHouseholdError, - error: householdError, - } = useUserHouseholds(userId); - - // Fetch geographic associations - const { - data: geographicData, - isLoading: isGeographicLoading, - isError: isGeographicError, - error: geographicError, - } = useGeographicAssociationsByUser(userId); + // 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 navigate = useNavigate(); @@ -49,18 +27,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`); @@ -75,134 +45,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) => { @@ -251,7 +133,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); @@ -278,46 +162,14 @@ 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 ( <> ); 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/Simulations.page.tsx b/app/src/pages/Simulations.page.tsx index 82abb64a0..b1555036a 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 { getCountryLabel, getRegionLabel, isNationalGeography } 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,9 @@ export default function SimulationsPage() { population: { text: item.userHousehold?.label || - item.geography?.name || + (item.geography + ? `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/pages/report-output/GeographySubPage.tsx b/app/src/pages/report-output/GeographySubPage.tsx index d59184886..4b7c10466 100644 --- a/app/src/pages/report-output/GeographySubPage.tsx +++ b/app/src/pages/report-output/GeographySubPage.tsx @@ -1,14 +1,36 @@ 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'; +import { useRegions } from '@/hooks/useRegions'; +import { Geography, isNationalGeography } from '@/types/ingredients/Geography'; +import { getCountryLabel, getRegionLabel, getRegionTypeLabel } from '@/utils/geographyUtils'; interface GeographySubPageProps { baselineGeography?: Geography; reformGeography?: Geography; - baselineUserGeography?: UserGeographyPopulation; - reformUserGeography?: UserGeographyPopulation; +} + +/** + * Get display scope label from geography using V2 API metadata + */ +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), + }; } /** @@ -16,35 +38,40 @@ interface GeographySubPageProps { * * Shows baseline and reform geographies side-by-side in a comparison table. * Collapses columns when both simulations use the same geography. + * Uses V2 API metadata to display human-readable region labels. */ export default function GeographySubPage({ baselineGeography, reformGeography, - baselineUserGeography, - reformUserGeography, }: 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
; } - // Check if geographies are the same - const geographiesAreSame = baselineGeography?.id === reformGeography?.id; + // Get display info for both geographies + const baselineInfo = useGeographyDisplayInfo(baselineGeography, regions); + const reformInfo = useGeographyDisplayInfo(reformGeography, regions); - // 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'; + // Check if geographies are the same by comparing regionCode + const geographiesAreSame = baselineGeography?.regionCode === reformGeography?.regionCode; - // Define table rows + // Define table rows using labels from V2 API const rows = [ { label: 'Geographic area', - baselineValue: baselineGeography?.name || '—', - reformValue: reformGeography?.name || '—', + baselineValue: isLoading ? '...' : baselineInfo.label, + reformValue: isLoading ? '...' : reformInfo.label, }, { label: 'Type', - baselineValue: baselineGeography?.scope ? capitalize(baselineGeography.scope) : '—', - reformValue: reformGeography?.scope ? capitalize(reformGeography.scope) : '—', + baselineValue: isLoading ? '...' : baselineInfo.scopeLabel, + reformValue: isLoading ? '...' : reformInfo.scopeLabel, }, ]; @@ -94,7 +121,7 @@ export default function GeographySubPage({ padding: `${spacing.md} ${spacing.lg}`, }} > - {baselineLabel.toUpperCase()} (BASELINE / REFORM) + {baselineInfo.label.toUpperCase()} (BASELINE / REFORM) ) : ( <> @@ -110,7 +137,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/pages/report-output/PopulationSubPage.tsx b/app/src/pages/report-output/PopulationSubPage.tsx index fecb672da..5be04438b 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,28 +53,18 @@ 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 user geography associations - const baselineUserGeography = userGeographies?.find( - (ug) => ug.geographyId === baselineGeographyId - ); - const reformUserGeography = userGeographies?.find((ug) => ug.geographyId === reformGeographyId); + // Find the geographies - match by regionCode + const baselineGeography = geographies?.find((g) => g.regionCode === baselineRegionCode); + const reformGeography = geographies?.find((g) => g.regionCode === reformRegionCode); return ( - + ); } diff --git a/app/src/pages/report-output/SocietyWideReportOutput.tsx b/app/src/pages/report-output/SocietyWideReportOutput.tsx index 967be7cb3..b18cd8d23 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'); @@ -78,10 +75,8 @@ export function SocietyWideReportOutput({ } const geography = { - id: `${report.countryId}-${simulation1.populationId}`, countryId: report.countryId, - scope: 'national' as const, - geographyId: simulation1.populationId || '', + regionCode: simulation1.populationId || report.countryId, }; return [ @@ -159,7 +154,6 @@ export function SocietyWideReportOutput({ baselineSimulation={simulations?.[0]} reformSimulation={simulations?.[1]} geographies={geographies} - userGeographies={userGeographies} /> ); 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..e948d8f37 100644 --- a/app/src/pathways/population/PopulationPathwayWrapper.tsx +++ b/app/src/pathways/population/PopulationPathwayWrapper.tsx @@ -1,30 +1,22 @@ /** - * PopulationPathwayWrapper - Pathway orchestrator for standalone population creation + * PopulationPathwayWrapper - Pathway orchestrator for standalone household creation * - * Manages local state for a single population (household or geographic). - * Reuses shared views from the report pathway with mode="standalone". + * Two-step flow: LABEL (name the household) → HOUSEHOLD_BUILDER (configure members). + * Geography selection is only available through the report/simulation pathways. */ import { useState } from 'react'; -import { useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; 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 { useRegionsList } from '@/hooks/useStaticMetadata'; -import { RootState } from '@/store'; import { Household } from '@/types/ingredients/Household'; import { StandalonePopulationViewMode } from '@/types/pathwayModes/PopulationViewMode'; import { PopulationStateProps } from '@/types/pathwayState'; -import { createPopulationCallbacks } from '@/utils/pathwayCallbacks'; -import { initializePopulationState } from '@/utils/pathwayState/initializePopulationState'; -import GeographicConfirmationView from '../report/views/population/GeographicConfirmationView'; import HouseholdBuilderView from '../report/views/population/HouseholdBuilderView'; import PopulationLabelView from '../report/views/population/PopulationLabelView'; -// Population views (reusing from report pathway) -import PopulationScopeView from '../report/views/population/PopulationScopeView'; interface PopulationPathwayWrapperProps { onComplete?: () => void; @@ -34,74 +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 currentYear = parseInt(CURRENT_YEAR, 10); - const regionData = useRegionsList(countryId, currentYear); - - // ========== 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; @@ -111,18 +66,7 @@ export default function PopulationPathwayWrapper({ onComplete }: PopulationPathw - ); - break; - - case StandalonePopulationViewMode.GEOGRAPHIC_CONFIRM: - currentView = ( - ); diff --git a/app/src/pathways/report/ReportPathwayWrapper.tsx b/app/src/pathways/report/ReportPathwayWrapper.tsx index c104eae78..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'; @@ -17,11 +16,9 @@ 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'; -import { RootState } from '@/store'; import { Report } from '@/types/ingredients/Report'; import { ReportViewMode } from '@/types/pathwayModes/ReportViewMode'; import { ReportStateProps, SimulationStateProps } from '@/types/pathwayState'; @@ -89,11 +86,8 @@ 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 reportYear = parseInt(reportState.year || '2025', 10); - const regionData = useRegionsList(countryId, reportYear); + const regionData = useRegionsList(countryId); // ========== NAVIGATION ========== const { currentMode, navigateToMode, goBack, canGoBack } = usePathwayNavigation( @@ -104,10 +98,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..cf452c6e1 100644 --- a/app/src/pathways/report/views/ReportSetupView.tsx +++ b/app/src/pathways/report/views/ReportSetupView.tsx @@ -1,9 +1,12 @@ import { useState } from 'react'; import PathwayView from '@/components/common/PathwayView'; import { MOCK_USER_ID } from '@/constants'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useRegions } from '@/hooks/useRegions'; import { useUserHouseholds } from '@/hooks/useUserHousehold'; +import { MetadataRegionEntry } from '@/types/metadata'; import { ReportStateProps, SimulationStateProps } from '@/types/pathwayState'; +import { getCountryLabel, getRegionLabel, isNationalGeography } from '@/utils/geographyUtils'; import { isSimulationConfigured } from '@/utils/validation/ingredientValidation'; type SimulationCard = 'simulation1' | 'simulation2'; @@ -32,16 +35,19 @@ 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 countryId = useCurrentCountry(); + const { regions } = useRegions(countryId); 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'; @@ -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 `Household #${simulation.population.household.id}`; + } + if (simulation?.population.geography) { + const geo = simulation.population.geography; + const label = isNationalGeography(geo) + ? getCountryLabel(geo.countryId) + : getRegionLabel(geo.regionCode, regions); + return `Households in ${label}`; + } + 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?.id || 'N/A'; - return `Policy #${policyId} • Household(s) #${populationId}`; + const populationLabel = getPopulationLabel(simulation, regions); + return `Policy #${policyId} • ${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?.id || 'N/A'; - return `Policy #${policyId} • Household(s) #${populationId}`; + const populationLabel = getPopulationLabel(simulation, regions); + return `Policy #${policyId} • ${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 a3e0c6d46..1c4baaaf7 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,23 @@ 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 in "Households in {label}" format + function getGeographyLabel(enhancedSim: EnhancedUserSimulation): string | undefined { + if (!enhancedSim.geography) { + return undefined; + } + const label = isNationalGeography(enhancedSim.geography) + ? getCountryLabel(enhancedSim.geography.countryId) + : getRegionLabel(enhancedSim.geography.regionCode, regions); + return `Households in ${label}`; + } + function canProceed() { if (!localSimulation) { return false; @@ -95,11 +111,9 @@ export default function ReportSimulationExistingView({ // Get other simulation's population ID (base ingredient ID) for compatibility check // For household populations, use household.id - // For geography populations, use geography.geographyId (the base geography identifier) + // For geography populations, use geography.regionCode const otherPopulationId = - otherSimulation?.population.household?.id || - otherSimulation?.population.geography?.geographyId || - otherSimulation?.population.geography?.id; + otherSimulation?.population.household?.id || otherSimulation?.population.geography?.regionCode; // Sort simulations to show compatible first, then incompatible const sortedSimulations = [...filteredSimulations].sort((a, b) => { @@ -130,7 +144,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 || getGeographyLabel(enhancedSim) || 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 925707f93..035e1754f 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'; @@ -37,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, }, }; } @@ -106,7 +102,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); @@ -141,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, @@ -158,6 +153,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 regionCode. */ async function createNewBaseline() { if (!onSelectDefaultBaseline) { @@ -166,21 +163,12 @@ export default function ReportSimulationSelectionView({ setIsCreatingBaseline(true); const countryName = countryNames[countryId] || countryId.toUpperCase(); + const regionCode = countryId; // National geography uses countryId as regionCode 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: regionCode, policyId: currentLawId.toString(), populationType: 'geography', }; @@ -193,11 +181,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, regionCode, countryName); const simulationState = createSimulationState( simulationId, simulationLabel, @@ -216,10 +200,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/ReportSubmitView.tsx b/app/src/pathways/report/views/ReportSubmitView.tsx index 7a188922d..0deb0db0d 100644 --- a/app/src/pathways/report/views/ReportSubmitView.tsx +++ b/app/src/pathways/report/views/ReportSubmitView.tsx @@ -31,7 +31,13 @@ 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}`; + (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}`; }; @@ -40,11 +46,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 4527ea17a..32a52ca3a 100644 --- a/app/src/pathways/report/views/population/GeographicConfirmationView.tsx +++ b/app/src/pathways/report/views/population/GeographicConfirmationView.tsx @@ -1,22 +1,22 @@ /** - * 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 + * + * 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 { MOCK_USER_ID } from '@/constants'; -import { useCreateGeographicAssociation } from '@/hooks/useUserGeographic'; -import { UserGeographyPopulation } from '@/types/ingredients/UserPopulation'; +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; regions: MetadataRegionEntry[]; - onSubmitSuccess: (geographyId: string, label: string) => void; + onSubmitSuccess: (regionCode: string, label: string) => void; onBack?: () => void; } @@ -26,36 +26,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 regionCode = population.geography.regionCode; + const label = getRegionLabel(regionCode, regions); + onSubmitSuccess(regionCode, label); }; // Build display content based on geographic scope @@ -69,52 +47,29 @@ export default function GeographicConfirmationView({ } const geographyCountryId = population.geography.countryId; + const regionCode = population.geography.regionCode; - if (population.geography.scope === 'national') { - return ( - - - Confirm household collection - - - Scope: National - - - Country: {getCountryLabel(geographyCountryId)} - - - ); - } - - // Subnational - const regionCode = population.geography.geographyId; - 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, - isLoading: isPending, }; return ( 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/pathways/report/views/population/PopulationExistingView.tsx b/app/src/pathways/report/views/population/PopulationExistingView.tsx index 84eeefda2..a9265c8de 100644 --- a/app/src/pathways/report/views/population/PopulationExistingView.tsx +++ b/app/src/pathways/report/views/population/PopulationExistingView.tsx @@ -1,21 +1,16 @@ /** - * 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'; import { Text } from '@mantine/core'; import { HouseholdAdapter } from '@/adapters'; import PathwayView from '@/components/common/PathwayView'; -import { CURRENT_YEAR, MOCK_USER_ID } from '@/constants'; -import { useCurrentCountry } from '@/hooks/useCurrentCountry'; -import { useRegionsList } from '@/hooks/useStaticMetadata'; -import { - isGeographicMetadataWithAssociation, - UserGeographicMetadataWithAssociation, - useUserGeographics, -} from '@/hooks/useUserGeographic'; +import { MOCK_USER_ID } from '@/constants'; 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,54 +31,24 @@ interface PopulationExistingViewProps { export default function PopulationExistingView({ onSelectHousehold, - onSelectGeography, onBack, onCancel, }: PopulationExistingViewProps) { const userId = MOCK_USER_ID.toString(); - const countryId = useCurrentCountry(); - const currentYear = parseInt(CURRENT_YEAR, 10); - const regions = useRegionsList(countryId, currentYear); - - // Fetch household populations - const { - data: householdData, - isLoading: isHouseholdLoading, - isError: isHouseholdError, - error: householdError, - } = useUserHouseholds(userId); - // Fetch geographic populations - const { - data: geographicData, - isLoading: isGeographicLoading, - isError: isGeographicError, - error: geographicError, - } = useUserGeographics(userId); + // Fetch household populations only + // Geographic populations are no longer stored as user associations + const { data: householdData, isLoading, isError, error } = useUserHouseholds(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) { @@ -96,28 +59,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; } @@ -135,7 +86,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 || ''; @@ -145,21 +96,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 ( @@ -181,7 +118,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/population/PopulationLabelView.tsx b/app/src/pathways/report/views/population/PopulationLabelView.tsx index dfad4fbe1..cfc686de8 100644 --- a/app/src/pathways/report/views/population/PopulationLabelView.tsx +++ b/app/src/pathways/report/views/population/PopulationLabelView.tsx @@ -8,9 +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; @@ -37,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 @@ -47,14 +50,13 @@ export default function PopulationLabelView({ if (population?.geography) { // Geographic population - if (population.geography.scope === 'national') { - return 'National Households'; - } else if (population.geography.geographyId) { - // Use display value (strip prefix for UK regions) - const displayValue = extractRegionDisplayValue(population.geography.geographyId); - return `${displayValue} Households`; + if (isNationalGeography(population.geography)) { + return 'Households nationwide'; + } else if (population.geography.regionCode) { + const label = getRegionLabel(population.geography.regionCode, regions); + return `Households in ${label}`; } - return 'Regional Households'; + return 'Regional households'; } // Household population return 'Custom Household'; diff --git a/app/src/pathways/report/views/population/PopulationScopeView.tsx b/app/src/pathways/report/views/population/PopulationScopeView.tsx index 265ae9508..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) => void; + onScopeSelected: ( + geography: Geography | null, + scopeType: ScopeType, + regionLabel?: string + ) => void; onBack?: () => void; onCancel?: () => void; } @@ -60,6 +64,32 @@ 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 +107,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/pathways/report/views/simulation/SimulationPopulationSetupView.tsx b/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx index 3d2a008a5..c119ed901 100644 --- a/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx +++ b/app/src/pathways/report/views/simulation/SimulationPopulationSetupView.tsx @@ -7,10 +7,12 @@ import { useState } from 'react'; import PathwayView from '@/components/common/PathwayView'; import { MOCK_USER_ID } from '@/constants'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; +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, @@ -41,9 +43,12 @@ export default function SimulationPopulationSetupView({ onCancel, }: SimulationPopulationSetupViewProps) { const userId = MOCK_USER_ID.toString(); + const countryId = useCurrentCountry(); + const { regions } = useRegions(countryId); 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); @@ -55,6 +60,23 @@ 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; + const label = isNationalGeography(geo) + ? getCountryLabel(geo.countryId) + : getRegionLabel(geo.regionCode, regions); + return `Households in ${label}`; + } + return 'Unknown household(s)'; + } + function handleClickCreateNew() { setSelectedAction('createNew'); } @@ -92,7 +114,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 f760799e9..9931f92e1 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,17 @@ export default function SimulationSetupView({ const canProceed: boolean = isPolicyConfigured(policy) && isPopulationConfigured(population); + // Helper to get geography display label in "Households in {label}" format + function getGeographyDisplayLabel(): string { + if (!population.geography) { + return ''; + } + const label = isNationalGeography(population.geography) + ? getCountryLabel(population.geography.countryId) + : getRegionLabel(population.geography.regionCode, regions); + return `Households in ${label}`; + } + function generatePopulationCardTitle() { if (!isPopulationConfigured(population)) { return 'Add household(s)'; @@ -81,7 +97,7 @@ export default function SimulationSetupView({ return `Household #${population.household.id}`; } if (population.geography) { - return `Household(s) #${population.geography.id}`; + return getGeographyDisplayLabel(); } return ''; } @@ -93,17 +109,14 @@ 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 || getGeographyDisplayLabel(); 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 ''; } diff --git a/app/src/pathways/report/views/simulation/SimulationSubmitView.tsx b/app/src/pathways/report/views/simulation/SimulationSubmitView.tsx index bdf55181f..1bcfb19df 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,23 @@ 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 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), - badge: - simulation.population.label || - `Household #${simulation.population.household?.id || simulation.population.geography?.id}`, + description: populationDisplay, + isFulfilled: !!populationIdentifier, + badge: populationDisplay, }, { title: 'Policy reform added', diff --git a/app/src/pathways/simulation/SimulationPathwayWrapper.tsx b/app/src/pathways/simulation/SimulationPathwayWrapper.tsx index 5295229dd..28354a5f2 100644 --- a/app/src/pathways/simulation/SimulationPathwayWrapper.tsx +++ b/app/src/pathways/simulation/SimulationPathwayWrapper.tsx @@ -6,17 +6,14 @@ */ 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'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; 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 { @@ -62,10 +59,8 @@ 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( @@ -76,10 +71,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..17fbe96ee 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,25 +56,14 @@ 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 => ({ 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, @@ -113,8 +99,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/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/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/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/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/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/useFetchReportIngredientsMocks.ts b/app/src/tests/fixtures/hooks/useFetchReportIngredientsMocks.ts index 7b45e94e9..dabdc1d34 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, @@ -64,19 +68,10 @@ 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: [], - userGeographies: [ - { - type: 'geography', - geographyId: TEST_IDS.GEOGRAPHIES.NATIONAL, - countryId: TEST_COUNTRIES.US, - scope: 'national', - label: 'United States', - }, - ], }; /** @@ -92,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', @@ -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/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/hooks/useSaveSharedReportMocks.ts b/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts index 15ef2a74d..c2edb274c 100644 --- a/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts +++ b/app/src/tests/fixtures/hooks/useSaveSharedReportMocks.ts @@ -44,24 +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: [], - 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 = { ...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' }, ], }; @@ -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..4c9844929 100644 --- a/app/src/tests/fixtures/hooks/useSharedReportDataMocks.tsx +++ b/app/src/tests/fixtures/hooks/useSharedReportDataMocks.tsx @@ -18,17 +18,8 @@ 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: [], - userGeographies: [ - { - type: 'geography', - geographyId: 'us', - countryId: 'us', - scope: 'national', - label: 'United States', - }, - ], }; export const MOCK_HOUSEHOLD_SHARE_DATA: ReportIngredientsInput = { @@ -39,11 +30,10 @@ 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' }, ], - 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..21b441133 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,57 +113,17 @@ export const mockHouseholdMetadata2 = { isError: false, }; -// Mock geographic metadata -export const mockGeography1: Geography = { - id: TEST_GEOGRAPHY_ID_1, - countryId: 'us' as any, - scope: 'national', - geographyId: 'us', -}; +// Note: Geographic metadata mocks removed - geographies are no longer stored as user associations -export const mockGeographyAssociation1: UserGeographyPopulation = { - id: 'association-3', - type: 'geography', - userId: 'user-123', - label: TEST_GEOGRAPHY_LABEL, +// Mock Geography objects (for use in simulations, not user associations) +export const mockGeography1: Geography = { 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, + regionCode: 'us', }; export const mockGeography2: Geography = { - id: TEST_GEOGRAPHY_ID_2, - countryId: 'us' as any, - scope: 'subnational', - 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, + regionCode: 'ca', }; // Mock hook return values @@ -193,26 +151,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/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/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/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/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/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/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/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..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', }; @@ -79,10 +80,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/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/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/pathways/report/views/PopulationViewMocks.ts b/app/src/tests/fixtures/pathways/report/views/PopulationViewMocks.ts index d269f8aaf..768ee5731 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'; @@ -30,15 +31,12 @@ export const mockPopulationStateWithHousehold: PopulationStateProps = { }; export const mockPopulationStateWithGeography: PopulationStateProps = { - label: 'National Households', + label: null, type: 'geography', household: null, geography: { - id: 'us-us', countryId: 'us', - geographyId: 'us', - scope: 'national', - name: 'United States', + regionCode: 'us', }, }; @@ -47,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/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/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/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/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/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/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/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 () => { 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/tests/unit/components/common/LazyNestedMenu.test.tsx b/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx index 894daf28e..3d49a0c58 100644 --- a/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx +++ b/app/src/tests/unit/components/common/LazyNestedMenu.test.tsx @@ -3,19 +3,15 @@ * * 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, 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'; @@ -31,12 +27,7 @@ describe('LazyNestedMenu', () => { const getChildren = createMockGetChildren(); // When - render( - - ); + render(); // Then expect(screen.getByText(TEST_NODE_LABELS.TAX)).toBeInTheDocument(); @@ -48,12 +39,7 @@ describe('LazyNestedMenu', () => { const getChildren = createMockGetChildren(); // When - render( - - ); + render(); // Then - no NavLink items should be rendered expect(screen.queryByRole('link')).not.toBeInTheDocument(); @@ -65,12 +51,7 @@ describe('LazyNestedMenu', () => { const getChildren = createMockGetChildren(); // When - render( - - ); + render(); // Then expect(screen.getByText('Parameter 1')).toBeInTheDocument(); @@ -83,12 +64,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 +80,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 +95,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 +115,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 +193,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 +205,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 +219,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 +240,7 @@ describe('LazyNestedMenu', () => { const getChildren = createMockGetChildren(); // When - render( - - ); + render(); // Then expect(getChildren).not.toHaveBeenCalled(); @@ -309,12 +250,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..e4d7a716a 100644 --- a/app/src/tests/unit/hooks/useLazyParameterTree.test.ts +++ b/app/src/tests/unit/hooks/useLazyParameterTree.test.ts @@ -3,18 +3,16 @@ * * 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, - 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'; @@ -27,8 +25,6 @@ vi.mock('react-redux', async () => { }; }); -import { useSelector } from 'react-redux'; - describe('useLazyParameterTree', () => { beforeEach(() => { vi.clearAllMocks(); 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 83c4e7729..3649923e7 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,13 @@ describe('useSharedReportData', () => { expect(result.current.isLoading).toBe(false); }); + // 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', - }); - - // User geography from ShareData - expect(result.current.userGeographies).toHaveLength(1); - expect(result.current.userGeographies[0]).toMatchObject({ - geographyId: 'us', - label: 'United States', - userId: 'shared', + regionCode: 'us', }); + // 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/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/useStaticMetadata.test.ts b/app/src/tests/unit/hooks/useStaticMetadata.test.ts index 6a14c7ccf..8269ef120 100644 --- a/app/src/tests/unit/hooks/useStaticMetadata.test.ts +++ b/app/src/tests/unit/hooks/useStaticMetadata.test.ts @@ -8,27 +8,26 @@ 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)', () => { 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/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/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/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/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 () => { 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/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 8cb1ad664..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 values + // Should display human-readable labels from V2 API expect(screen.getByText('California')).toBeInTheDocument(); expect(screen.getByText('New York')).toBeInTheDocument(); - expect(screen.getAllByText('Subnational')).toHaveLength(2); // One for baseline, one for reform + expect(screen.getAllByText('State')).toHaveLength(2); // Region type label }); test('given same geography then displays value once', () => { @@ -89,9 +108,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 label once per row (merged column) + const caElements = screen.getAllByText('California'); + 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..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,11 +71,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 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..86b8d4821 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, + mockUseParams, + mockUsePathwayNavigationReturn, + mockUseRegionsEmpty, + 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,48 +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/useUserGeographic', () => ({ - useCreateGeographicAssociation: vi.fn(() => ({ mutateAsync: 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/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/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/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/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/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/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/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..fc4889e81 100644 --- a/app/src/tests/unit/utils/populationCompatibility.test.ts +++ b/app/src/tests/unit/utils/populationCompatibility.test.ts @@ -106,26 +106,15 @@ 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 - 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', () => { @@ -152,7 +141,7 @@ describe('populationCompatibility', () => { const result = getPopulationLabel(population); // Then - expect(result).toBe('Unknown Household(s)'); + expect(result).toBe('Unknown household(s)'); }); }); 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/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 }); }); }); 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/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/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..8bdeff9d1 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'; @@ -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); } /** @@ -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 */ @@ -94,11 +87,4 @@ export function isUserReport(obj: UserIngredient): obj is UserReport { // Export all types export type { Geography, Household, Policy, Population, Report, Simulation }; -export type { - UserPolicy, - UserGeographyPopulation, - UserHouseholdPopulation, - UserPopulation, - UserReport, - UserSimulation, -}; +export type { UserPolicy, UserHouseholdPopulation, UserPopulation, UserReport, UserSimulation }; 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', } 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/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; } diff --git a/app/src/utils/PopulationOps.ts b/app/src/utils/PopulationOps.ts index 6b58f5df8..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; @@ -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 @@ -61,7 +44,7 @@ export const PopulationOps = { getId: (p: PopulationRef): string => matchPopulation(p, { household: (h) => h.householdId, - geography: (g) => g.geographyId, + geography: (g) => g.regionCode, }), /** @@ -70,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}`, }), /** @@ -94,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, }), @@ -105,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}`, }), /** @@ -114,18 +96,15 @@ 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, }), /** * 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 }; }, /** @@ -149,35 +128,26 @@ export const PopulationOps = { /** * Create a geography population reference */ - geography: (geographyId: string): GeographyPopulationRef => ({ + geography: (regionCode: string): GeographyPopulationRef => ({ type: 'geography', - geographyId, + regionCode, }), }; /** - * 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 +157,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/geographyUtils.ts b/app/src/utils/geographyUtils.ts index 85ae234e2..46070eb0c 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 { isNationalGeography, type Geography } 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 107bcc154..dcfd48d23 100644 --- a/app/src/utils/pathwayCallbacks/populationCallbacks.ts +++ b/app/src/utils/pathwayCallbacks/populationCallbacks.ts @@ -38,18 +38,49 @@ 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 ? `Households in ${regionLabel}` : '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.regionCode, 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( @@ -68,11 +99,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', }) @@ -110,20 +141,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..5b24a4079 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,17 +47,12 @@ 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 (fallback when region metadata unavailable) + if (population.geography?.regionCode) { + return `Households in ${population.geography.regionCode}`; } - return 'Unknown Household(s)'; + return 'Unknown household(s)'; } /** 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/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/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..4ad6fe350 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'; @@ -27,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 @@ -35,7 +34,7 @@ export function isPopulationConfigured( if (!population) { return false; } - return !!(population.household?.id || population.geography?.id); + return !!(population.household?.id || population.geography?.regionCode); } /** @@ -131,33 +130,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; -}