Skip to content
Draft
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
53 changes: 53 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,55 @@ 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`;

console.log('[updateSimulationOutput] Updating simulation:', simulationId, 'with output');

const payload = {
id: parseInt(simulationId, 10),
output_json: simulation.output ? JSON.stringify(simulation.output.householdData) : 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;
}
135 changes: 82 additions & 53 deletions app/src/libs/calculations/manager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { QueryClient } from '@tanstack/react-query';
import { markReportCompleted, markReportError } from '@/api/report';
import { CalculationMeta } from '@/api/reportCalculations';
import { updateSimulationOutput } from '@/api/simulation';
import { countryIds } from '@/libs/countries';
import { Report, ReportOutput } from '@/types/ingredients/Report';
import { Simulation } from '@/types/ingredients/Simulation';
import { HouseholdProgressUpdater } from './progressUpdater';
import { CalculationService, getCalculationService } from './service';
import { CalculationStatusResponse } from './status';
Expand Down Expand Up @@ -32,43 +34,56 @@ export class CalculationManager {
reportId: string,
meta: CalculationMeta
): Promise<CalculationStatusResponse> {
// Create completion callback for household calculations
const onComplete = async (completedReportId: string, status: 'ok' | 'error', result?: any) => {
console.log(
'[CalculationManager] Household calculation completed:',
completedReportId,
status
);

// Check if we haven't already updated this report
if (!this.reportStatusTracking.get(completedReportId)) {
this.reportStatusTracking.set(completedReportId, true);
await this.updateReportStatus(
completedReportId,
status === 'ok' ? 'complete' : 'error',
meta.countryId,
result
);
}
// Create callbacks
const callbacks = {
onComplete: async (completedReportId: string, status: 'ok' | 'error', result?: any) => {
console.log('[CalculationManager] Calculation completed:', completedReportId, status);

if (!this.reportStatusTracking.get(completedReportId)) {
this.reportStatusTracking.set(completedReportId, true);
await this.updateReportStatus(
completedReportId,
status === 'ok' ? 'complete' : 'error',
meta.countryId,
result,
meta
);
}
},
onSimulationComplete:
meta.type === 'household'
? async (simulationId: string, result: any, policyId: string) => {
console.log('[CalculationManager] Simulation completed:', simulationId);

const simulation: Simulation = {
id: simulationId,
countryId: meta.countryId,
policyId,
populationId: meta.populationId,
populationType: 'household',
label: null,
isCreated: true,
output: result,
};

await updateSimulationOutput(meta.countryId, simulationId, simulation);
}
: undefined,
};

// Execute calculation with callback for household
const result = await this.service.executeCalculation(
reportId,
meta,
meta.type === 'household' ? onComplete : undefined
);
// Execute calculation with callbacks
const result = await this.service.executeCalculation(reportId, meta, callbacks);

// For economy calculations, update immediately if complete
// (household updates happen via callback)
if (meta.type === 'economy' && !this.reportStatusTracking.get(reportId)) {
if (result.status === 'ok' || result.status === 'error') {
this.reportStatusTracking.set(reportId, true);
await this.updateReportStatus(
reportId,
result.status === 'ok' ? 'complete' : 'error',
meta.countryId,
result.result
result.result,
meta
);
}
}
Expand All @@ -85,35 +100,44 @@ export class CalculationManager {
this.reportStatusTracking.delete(reportId);

if (meta.type === 'household') {
// For household, check if already running
const handler = this.service.getHandler('household');
if (!handler.isActive(reportId)) {
// Create completion callback for household
const onComplete = async (
completedReportId: string,
status: 'ok' | 'error',
result?: any
) => {
console.log(
'[CalculationManager.startCalculation] Household completed:',
completedReportId,
status
);
// Create callbacks
const callbacks = {
onComplete: async (completedReportId: string, status: 'ok' | 'error', result?: any) => {
console.log('[CalculationManager] Calculation completed:', completedReportId, status);

if (!this.reportStatusTracking.get(completedReportId)) {
this.reportStatusTracking.set(completedReportId, true);
await this.updateReportStatus(
completedReportId,
status === 'ok' ? 'complete' : 'error',
meta.countryId,
result,
meta
);
}
},
onSimulationComplete: async (simulationId: string, result: any, policyId: string) => {
console.log('[CalculationManager] Simulation completed:', simulationId);

const simulation: Simulation = {
id: simulationId,
countryId: meta.countryId,
policyId,
populationId: meta.populationId,
populationType: 'household',
label: null,
isCreated: true,
output: result,
};

// Check if we haven't already updated this report
if (!this.reportStatusTracking.get(completedReportId)) {
this.reportStatusTracking.set(completedReportId, true);
await this.updateReportStatus(
completedReportId,
status === 'ok' ? 'complete' : 'error',
meta.countryId,
result
);
}
await updateSimulationOutput(meta.countryId, simulationId, simulation);
},
};

// Start the calculation with callback
await this.service.executeCalculation(reportId, meta, onComplete);
// Start the calculation with callbacks
await this.service.executeCalculation(reportId, meta, callbacks);
// Start progress updates
this.progressUpdater.startProgressUpdates(reportId, handler as any);
}
Expand Down Expand Up @@ -152,19 +176,24 @@ export class CalculationManager {

/**
* Update the report status in the database when a calculation completes or errors
* For household calculations, output is null (stored in Simulations)
* For economy calculations, output contains the comparison results
*/
async updateReportStatus(
reportId: string,
status: 'complete' | 'error',
countryId: (typeof countryIds)[number],
result?: ReportOutput
result?: ReportOutput,
calculationMeta?: CalculationMeta
): Promise<void> {
// Create a minimal Report object with just the necessary fields
// Both household and society-wide results are stored in the output field
// For household: output is null (stored in Simulation)
// For economy: output contains the comparison results
const report: Report = {
id: reportId,
status,
output: status === 'complete' ? result || null : null,
output:
status === 'complete' && calculationMeta?.type !== 'household' ? result || null : null,
countryId,
apiVersion: '',
simulationIds: [],
Expand Down
50 changes: 47 additions & 3 deletions app/src/libs/calculations/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ export class CalculationService {
? geography.geographyId
: undefined;

// Collect simulation IDs to update with calculation results
const simulationIds: string[] = [];
if (simulation1.id) {
simulationIds.push(simulation1.id);
}
if (simulation2?.id) {
simulationIds.push(simulation2.id);
}

return {
type,
countryId: countryId as any,
Expand All @@ -74,6 +83,7 @@ export class CalculationService {
},
populationId,
region,
simulationIds,
};
}

Expand Down Expand Up @@ -113,15 +123,49 @@ export class CalculationService {
* Execute a calculation through the appropriate handler
* @param reportId - The report ID
* @param meta - The calculation metadata
* @param onComplete - Optional callback for household calculation completion
* @param callbacks - Optional callbacks for completion events
*/
async executeCalculation(
reportId: string,
meta: CalculationMeta,
onComplete?: (reportId: string, status: 'ok' | 'error', result?: any) => Promise<void>
callbacks?: {
onComplete?: (reportId: string, status: 'ok' | 'error', result?: any) => Promise<void>;
onSimulationComplete?: (simulationId: string, result: any, policyId: string) => Promise<void>;
}
): Promise<CalculationStatusResponse> {
if (meta.type === 'household') {
return this.householdHandler.execute(reportId, meta, onComplete);
// Loop through each simulation and run calculation
for (let index = 0; index < meta.simulationIds.length; index++) {
const simulationId = meta.simulationIds[index];
const policyId = index === 0 ? meta.policyIds.baseline : meta.policyIds.reform;

if (!policyId) {
continue;
}

const singleSimMeta: CalculationMeta = {
...meta,
policyIds: { baseline: policyId, reform: undefined },
simulationIds: [simulationId],
};

await this.householdHandler.execute(
`${reportId}-sim-${simulationId}`,
singleSimMeta,
async (_, status, res) => {
if (status === 'ok' && callbacks?.onSimulationComplete) {
await callbacks.onSimulationComplete(simulationId, res, policyId);
}
}
);
}

// Notify overall completion
if (callbacks?.onComplete) {
await callbacks.onComplete(reportId, 'ok', null);
}

return { status: 'ok', result: null };
}
return this.economyHandler.execute(reportId, meta);
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/libs/queryOptions/calculations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ async function getOrReconstructMetadata(
sim1.population_type === 'geography' && sim1.population_id !== report.country_id
? String(sim1.population_id)
: undefined,
// Simulation IDs needed for storing household calculation outputs
simulationIds: [
String(report.simulation_1_id),
...(report.simulation_2_id ? [String(report.simulation_2_id)] : []),
],
};

console.log(
Expand Down
Loading
Loading