Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c74559c
feat: First IndexedDB implementation
anth-volk Dec 18, 2025
b797a56
feat: First API fetching code
anth-volk Dec 18, 2025
23f6075
feat: Loaders for parameters, variables, datasets
anth-volk Dec 19, 2025
e34b97e
feat: Metadata bulk loading and tests
anth-volk Dec 19, 2025
614cff6
feat: Metadata guard
anth-volk Dec 19, 2025
8025704
feat: Static data
anth-volk Dec 19, 2025
7d20af4
feat: Regions
anth-volk Dec 19, 2025
38635e1
feat: Begin using migrated metadata endpoints in existing hooks; fix …
anth-volk Dec 22, 2025
1d76ec9
fix: Remove unused function
anth-volk Dec 22, 2025
628e990
test: Add tests
anth-volk Dec 22, 2025
07bb8d6
fix: Fix imports
anth-volk Dec 23, 2025
3d93f24
fix: Continue fixing metadata to use one-stage loading approach
anth-volk Dec 23, 2025
c3dd97e
test: Fix tests
anth-volk Dec 23, 2025
3dd9926
test: Update tests
anth-volk Dec 23, 2025
bb2fc8e
fix: Fix loading into IndexedDB
anth-volk Dec 24, 2025
72f73c4
fix: Rename metadata loader; remove unneeded compatibility features
anth-volk Dec 24, 2025
59d988f
fix: Move toward saving v2 variables and params in Redux
anth-volk Dec 26, 2025
4d84c9c
feat: Fetch parameter values from API v2
anth-volk Dec 26, 2025
d176351
test: Add tests
anth-volk Dec 29, 2025
90ad5ba
feat: Use new parameter value features
anth-volk Dec 29, 2025
fd162d7
fix: Properly pass metadata to household overview page in report output
anth-volk Dec 29, 2025
7e4bbf0
fix: Fix current law IDs
anth-volk Dec 29, 2025
5c285f9
fix: Create new param metadata tree builder func
anth-volk Dec 29, 2025
6c41ae9
feat: Use new tax_benefit_model_name URL param
anth-volk Jan 12, 2026
8be1167
fix: Add loading
anth-volk Jan 19, 2026
5114d3e
fix: Fix old hook usage
anth-volk Jan 19, 2026
eb2fd19
fix: Fix various tests
anth-volk Jan 19, 2026
59df97e
fix: Remove unused features from API v1 metadata
anth-volk Jan 19, 2026
74e69a6
fix: Simplify metadata handler
anth-volk Jan 20, 2026
bba58dc
fix: Remove deprecated variable handling; chore: Lint
anth-volk Jan 20, 2026
2444daa
feat: Lazy param tree builder
anth-volk Jan 20, 2026
b4af1de
fix: Remove unused code
anth-volk Jan 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@tanstack/react-query": "^5.84.1",
"@tanstack/react-query-devtools": "^5.83.0",
"dayjs": "^1.11.13",
"dexie": "^4.2.1",
"framer-motion": "^12.23.24",
"fuse.js": "^7.0.0",
"jsonp": "^0.2.1",
Expand Down Expand Up @@ -71,6 +72,7 @@
"@types/react-plotly.js": "^2.6.3",
"@types/react-syntax-highlighter": "^15.5.0",
"@types/topojson-client": "^3.1.5",
"@types/topojson-specification": "^1.0.5",
"@types/wordwrapjs": "^5.1.2",
"@vercel/node": "^5.5.10",
"@vitejs/plugin-react": "^4.5.2",
Expand Down
75 changes: 28 additions & 47 deletions app/src/CalculatorRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import ReportPathwayWrapper from './pathways/report/ReportPathwayWrapper';
import SimulationPathwayWrapper from './pathways/simulation/SimulationPathwayWrapper';
import { CountryGuard } from './routing/guards/CountryGuard';
import { MetadataGuard } from './routing/guards/MetadataGuard';
import { MetadataLazyLoader } from './routing/guards/MetadataLazyLoader';
import { RedirectToCountry } from './routing/RedirectToCountry';

/**
Expand All @@ -43,55 +42,11 @@ const router = createBrowserRouter(
path: '/:countryId',
element: <CountryGuard />,
children: [
// Routes with standard layout that need metadata (blocking)
// Layout is OUTSIDE the guard so app shell remains visible during loading
{
element: <StandardLayoutOutlet />,
children: [
{
element: <MetadataGuard />,
children: [
{
path: 'report-output/:reportId/:subpage?/:view?',
element: <ReportOutputPage />,
},
],
},
],
},
// Pathway routes that need metadata (blocking)
// Pathways manage their own AppShell layouts - do NOT wrap in StandardLayoutOutlet
// This allows views like PolicyParameterSelectorView to use custom AppShell configurations
// All routes need metadata (variables, datasets, parameters)
{
element: <MetadataGuard />,
children: [
{
element: <PathwayLayout />,
children: [
{
path: 'reports/create',
element: <ReportPathwayWrapper />,
},
{
path: 'simulations/create',
element: <SimulationPathwayWrapper />,
},
{
path: 'households/create',
element: <PopulationPathwayWrapper />,
},
{
path: 'policies/create',
element: <PolicyPathwayWrapper />,
},
],
},
],
},
// Routes that benefit from metadata but don't require it (lazy loader)
{
element: <MetadataLazyLoader />,
children: [
// Routes with StandardLayout
{
element: <StandardLayoutOutlet />,
children: [
Expand Down Expand Up @@ -123,6 +78,32 @@ const router = createBrowserRouter(
path: 'account',
element: <div>Account settings page</div>,
},
{
path: 'report-output/:reportId/:subpage?/:view?',
element: <ReportOutputPage />,
},
],
},
// Pathway routes
{
element: <PathwayLayout />,
children: [
{
path: 'reports/create',
element: <ReportPathwayWrapper />,
},
{
path: 'simulations/create',
element: <SimulationPathwayWrapper />,
},
{
path: 'households/create',
element: <PopulationPathwayWrapper />,
},
{
path: 'policies/create',
element: <PolicyPathwayWrapper />,
},
],
},
],
Expand Down
6 changes: 4 additions & 2 deletions app/src/adapters/HouseholdAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { getEntities } from '@/data/static';
import { countryIds } from '@/libs/countries';
import { store } from '@/store';
import { Household, HouseholdData } from '@/types/ingredients/Household';
import { HouseholdMetadata } from '@/types/metadata/householdMetadata';
import { HouseholdCreationPayload } from '@/types/payloads';

/**
* Get entity metadata from the Redux store
* Get entity metadata from static data based on current country
*/
function getEntityMetadata() {
const state = store.getState();
return state.metadata?.entities || {};
const countryId = state.metadata?.currentCountry || 'us';
return getEntities(countryId);
}

/**
Expand Down
122 changes: 122 additions & 0 deletions app/src/adapters/MetadataAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
ParameterMetadata,
V2DatasetMetadata,
V2ParameterMetadata,
V2ParameterValueMetadata,
V2VariableMetadata,
VariableMetadata,
} from '@/types/metadata';
import { ValuesList } from '@/types/subIngredients/valueInterval';

/**
* Dataset type used in the frontend (simplified from V2DatasetMetadata)
*/
export interface DatasetEntry {
name: string;
label: string;
title: string;
default: boolean;
}

/**
* Adapter for converting between V2 API metadata and internal formats
*/
export class MetadataAdapter {
/**
* Convert a single V2 variable to frontend VariableMetadata
*/
static variableFromV2(v2: V2VariableMetadata): VariableMetadata {
return {
id: v2.id,
name: v2.name,
entity: v2.entity,
description: v2.description,
data_type: v2.data_type,
possible_values: v2.possible_values,
tax_benefit_model_version_id: v2.tax_benefit_model_version_id,
created_at: v2.created_at,
// Auto-generate label from name (sentence case)
// TODO: V2 API should provide labels like V1 API did
label: v2.name.replace(/_/g, ' ').replace(/^./, (c: string) => c.toUpperCase()),
};
}

/**
* Convert V2 variables array to a keyed record
*/
static variablesFromV2(variables: V2VariableMetadata[]): Record<string, VariableMetadata> {
const record: Record<string, VariableMetadata> = {};
for (const v of variables) {
record[v.name] = MetadataAdapter.variableFromV2(v);
}
return record;
}

/**
* Convert a single V2 parameter to frontend ParameterMetadata
*/
static parameterFromV2(p: V2ParameterMetadata): ParameterMetadata {
return {
id: p.id,
name: p.name,
label: p.label,
description: p.description,
unit: p.unit,
data_type: p.data_type,
tax_benefit_model_version_id: p.tax_benefit_model_version_id,
created_at: p.created_at,
parameter: p.name, // Use name as parameter path
type: 'parameter',
values: {}, // Parameter values are fetched on-demand
};
}

/**
* Convert V2 parameters array to a keyed record
*/
static parametersFromV2(parameters: V2ParameterMetadata[]): Record<string, ParameterMetadata> {
const record: Record<string, ParameterMetadata> = {};
for (const p of parameters) {
record[p.name] = MetadataAdapter.parameterFromV2(p);
}
return record;
}

/**
* Convert a single V2 dataset to frontend DatasetEntry
* @param d V2 dataset
* @param isDefault Whether this is the default dataset
*/
static datasetFromV2(d: V2DatasetMetadata, isDefault: boolean): DatasetEntry {
return {
name: d.name,
label: d.name,
title: d.description || d.name,
default: isDefault,
};
}

/**
* Convert V2 datasets array to frontend format
* First dataset is marked as default
*/
static datasetsFromV2(datasets: V2DatasetMetadata[]): DatasetEntry[] {
return datasets.map((d, i) => MetadataAdapter.datasetFromV2(d, i === 0));
}

/**
* Convert V2 parameter values array to ValuesList format
* ValuesList is a Record<startDate, value> used by the frontend
* @param values Array of V2 parameter value records
* @returns ValuesList format (e.g., { "2023-01-01": 100, "2024-01-01": 150 })
*/
static parameterValuesFromV2(values: V2ParameterValueMetadata[]): ValuesList {
const valuesList: ValuesList = {};
for (const v of values) {
// Convert ISO timestamp (e.g., "2025-01-01T00:00:00") to date string (e.g., "2025-01-01")
const dateKey = v.start_date.split('T')[0];
valuesList[dateKey] = v.value_json;
}
return valuesList;
}
}
2 changes: 2 additions & 0 deletions app/src/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export { PolicyAdapter } from './PolicyAdapter';
export { SimulationAdapter } from './SimulationAdapter';
export { ReportAdapter } from './ReportAdapter';
export { HouseholdAdapter } from './HouseholdAdapter';
export { MetadataAdapter } from './MetadataAdapter';
export type { DatasetEntry } from './MetadataAdapter';

// User Ingredient Adapters
export { UserReportAdapter } from './UserReportAdapter';
Expand Down
25 changes: 0 additions & 25 deletions app/src/api/metadata.ts

This file was deleted.

16 changes: 16 additions & 0 deletions app/src/api/v2/datasets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { V2DatasetMetadata } from '@/types/metadata';
import { API_V2_BASE_URL, getModelName } from './taxBenefitModels';

/**
* Fetch all datasets for a country
*/
export async function fetchDatasets(countryId: string): Promise<V2DatasetMetadata[]> {
const modelName = getModelName(countryId);
const res = await fetch(`${API_V2_BASE_URL}/datasets/?tax_benefit_model_name=${modelName}`);

if (!res.ok) {
throw new Error(`Failed to fetch datasets for ${countryId}`);
}

return res.json();
}
23 changes: 23 additions & 0 deletions app/src/api/v2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Tax benefit models
export {
API_V2_BASE_URL,
COUNTRY_TO_MODEL_NAME,
getModelName,
fetchTaxBenefitModels,
fetchModelVersion,
fetchModelVersionId,
type TaxBenefitModel,
type TaxBenefitModelVersion,
} from './taxBenefitModels';

// Variables
export { fetchVariables } from './variables';

// Parameters
export { fetchParameters } from './parameters';

// Parameter values (on-demand fetching)
export { fetchParameterValues, BASELINE_POLICY_ID } from './parameterValues';

// Datasets
export { fetchDatasets } from './datasets';
43 changes: 43 additions & 0 deletions app/src/api/v2/parameterValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { V2ParameterValueMetadata } from '@/types/metadata';
import { API_V2_BASE_URL } from './taxBenefitModels';

/**
* Special constant to indicate baseline (current law) values.
* Baseline values have policy_id = null in the database.
* When fetching baseline values, we omit the policy_id parameter entirely.
*/
export const BASELINE_POLICY_ID = 'baseline' as const;

/**
* Fetch parameter values for a specific parameter and policy.
*
* All parameter values are stored separately and linked to a policy.
* Both baseline (current law) and reform policies have their own sets of values.
*
* @param parameterId - The ID of the parameter to fetch values for
* @param policyId - The ID of the policy, or "baseline" for current law values (omits policy_id from query)
* @returns Array of parameter value records
*/
export async function fetchParameterValues(
parameterId: string,
policyId: string
): Promise<V2ParameterValueMetadata[]> {
const params = new URLSearchParams();
params.set('parameter_id', parameterId);

// For baseline (current law), omit policy_id to get values where policy_id IS NULL
// For reform policies, include the actual policy UUID
if (policyId !== BASELINE_POLICY_ID) {
params.set('policy_id', policyId);
}

const res = await fetch(`${API_V2_BASE_URL}/parameter-values/?${params.toString()}`);

if (!res.ok) {
throw new Error(
`Failed to fetch parameter values for parameter ${parameterId} with policy ${policyId}`
);
}

return res.json();
}
18 changes: 18 additions & 0 deletions app/src/api/v2/parameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { V2ParameterMetadata } from '@/types/metadata';
import { API_V2_BASE_URL, getModelName } from './taxBenefitModels';

/**
* Fetch all parameters for a country.
*/
export async function fetchParameters(countryId: string): Promise<V2ParameterMetadata[]> {
const modelName = getModelName(countryId);
const res = await fetch(
`${API_V2_BASE_URL}/parameters/?tax_benefit_model_name=${modelName}&limit=10000`
);

if (!res.ok) {
throw new Error(`Failed to fetch parameters for ${countryId}`);
}

return res.json();
}
Loading