Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions app/src/adapters/SimulationAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Household } from '@/types/ingredients/Household';
import { Simulation } from '@/types/ingredients/Simulation';
import { SimulationMetadata } from '@/types/metadata/simulationMetadata';
import { SimulationCreationPayload } from '@/types/payloads';
Expand All @@ -24,6 +25,20 @@
throw new Error('Simulation metadata missing population_type');
}

// Parse output_json if present
let output: Household | null = null;
if (metadata.output_json) {
try {
const householdData = JSON.parse(metadata.output_json);
output = {
countryId: metadata.country_id,
householdData,
};
} catch (error) {
console.error('[SimulationAdapter] Failed to parse output_json:', error);

Check warning on line 38 in app/src/adapters/SimulationAdapter.ts

View workflow job for this annotation

GitHub Actions / Lint and format

Unexpected console statement
}
}

return {
id: String(metadata.id),
countryId: metadata.country_id,
Expand All @@ -33,6 +48,7 @@
populationType,
label: null,
isCreated: true,
output,
};
}

Expand Down
3 changes: 2 additions & 1 deletion app/src/api/reportCalculations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EconomyCalculationParams, fetchEconomyCalculation } from './economy';
import { fetchHouseholdCalculation } from './householdCalculation';

/**
* Metadata needed to fetch a calculation
* Metadata needed to fetch a calculation and store results
* This is stored alongside the calculation in the cache when a report is created
*/
export interface CalculationMeta {
Expand All @@ -16,6 +16,7 @@ export interface CalculationMeta {
};
populationId: string;
region?: string;
simulationIds: string[]; // Track which simulations to update with calculation results
}

/**
Expand Down
51 changes: 51 additions & 0 deletions app/src/api/simulation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BASE_URL } from '@/constants';
import { countryIds } from '@/libs/countries';
import { Simulation } from '@/types/ingredients/Simulation';
import { SimulationMetadata } from '@/types/metadata/simulationMetadata';
import { SimulationCreationPayload } from '@/types/payloads';

Expand Down Expand Up @@ -76,3 +77,53 @@ export async function createSimulation(
},
};
}

/**
* Update a simulation with calculation output
* NOTE: Follows the same pattern as report PATCH endpoint (ID in payload, not URL)
*
* @param countryId - The country ID
* @param simulationId - The simulation ID
* @param simulation - The simulation object with output
* @returns The updated simulation metadata
*/
export async function updateSimulationOutput(
countryId: (typeof countryIds)[number],
simulationId: string,
simulation: Simulation
): Promise<SimulationMetadata> {
const url = `${BASE_URL}/${countryId}/simulation`;

const payload = {
id: parseInt(simulationId, 10),
output_json: simulation.output ? JSON.stringify(simulation.output) : null,
};

const response = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(payload),
});

if (!response.ok) {
throw new Error(
`Failed to update simulation ${simulationId}: ${response.status} ${response.statusText}`
);
}

let json;
try {
json = await response.json();
} catch (error) {
throw new Error(`Failed to parse simulation update response: ${error}`);
}

if (json.status !== 'ok') {
throw new Error(json.message || `Failed to update simulation ${simulationId}`);
}

return json.result;
}
61 changes: 43 additions & 18 deletions app/src/hooks/useReportData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useQueryClient } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { EconomyReportOutput } from '@/api/economy';
import { CalculationMeta } from '@/api/reportCalculations';
import { Household, HouseholdData } from '@/types/ingredients/Household';
import { fetchSimulationById } from '@/api/simulation';
import { Household } from '@/types/ingredients/Household';
import { useReportOutput } from './useReportOutput';
import { useUserReportById } from './useUserReports';

Expand Down Expand Up @@ -99,14 +100,27 @@ export function useReportData(userReportId: string): ReportDataResult {
} = normalizedReport;
const baseReportId = report?.id;

// Step 2: Fetch report output using base reportId
// Step 2: Get metadata early to determine if we need simulation data
const metadata = queryClient.getQueryData<CalculationMeta>(['calculation-meta', baseReportId]);
const outputType: ReportOutputType | undefined = metadata?.type;
const reformSimId = metadata?.simulationIds?.[1] || metadata?.simulationIds?.[0];

// Step 3: Fetch report output using base reportId
// This hook must be called unconditionally to comply with Rules of Hooks
const reportOutputResult = useReportOutput({
reportId: baseReportId || '',
enabled: !!baseReportId, // Only enable when we have a valid base report ID
});

// Step 3: Handle loading and error states
// Step 4: Fetch simulation output for household reports
// This hook must be called unconditionally to comply with Rules of Hooks
const { data: simulationData } = useQuery({
queryKey: ['simulation', reformSimId || 'none'],
queryFn: () => fetchSimulationById(metadata!.countryId, reformSimId!),
enabled: !!baseReportId && outputType === 'household' && !!reformSimId,
Comment on lines +117 to +120
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Are you doing only the reform because in a next PR, you'll add the code for the comparison?

});

// Step 5: Handle loading and error states
if (normalizedLoading) {
return LOADING_PROPS;
}
Expand All @@ -118,7 +132,7 @@ export function useReportData(userReportId: string): ReportDataResult {
};
}

// Step 4: Process the report output result
// Step 6: Process the report output result
const { status, data, error } = reportOutputResult;

// Extract progress information if status is pending
Expand All @@ -132,21 +146,32 @@ export function useReportData(userReportId: string): ReportDataResult {
estimatedTimeRemaining: undefined,
};

// Determine output type from cached metadata
const metadata = queryClient.getQueryData<CalculationMeta>(['calculation-meta', baseReportId]);
const outputType: ReportOutputType | undefined = metadata?.type;

// Wrap household data in Household structure
// The API returns raw HouseholdData, but components expect the Household wrapper
// For economy reports, use the report output directly
// For household reports, construct from simulation output
let output: EconomyReportOutput | Household | null | undefined = data;

if (outputType === 'household' && data) {
const wrappedOutput: Household = {
id: baseReportId,
countryId: metadata?.countryId || 'us',
householdData: data as HouseholdData,
};
output = wrappedOutput;
if (outputType === 'household' && simulationData?.output_json) {
const parsed =
typeof simulationData.output_json === 'string'
? JSON.parse(simulationData.output_json)
: simulationData.output_json;

// Check if already wrapped (has countryId and householdData properties)
if (parsed.countryId && parsed.householdData) {
// Already a Household object, just add the ID
output = {
...parsed,
id: baseReportId,
};
} else {
// Raw HouseholdData, need to wrap it
const wrappedOutput: Household = {
id: baseReportId,
countryId: metadata!.countryId,
householdData: parsed,
};
output = wrappedOutput;
}
Comment on lines +160 to +174
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why is this necessary?

}

return {
Expand Down
Loading
Loading