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
14 changes: 12 additions & 2 deletions app/src/adapters/ReportAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Report } from '@/types/ingredients/Report';
import { ReportMetadata } from '@/types/metadata/reportMetadata';
import { ReportCreationPayload } from '@/types/payloads/ReportCreationPayload';
import { ReportSetOutputPayload } from '@/types/payloads/ReportSetOutputPayload';
import type { RunMetadata } from '@/types/runMetadata';
import { convertJsonToReportOutput, convertReportOutputToJson } from './conversionHelpers';

/**
Expand Down Expand Up @@ -65,28 +66,37 @@ export class ReportAdapter {
/**
* Creates payload for marking a report as completed with output
*/
static toCompletedReportPayload(report: Report): ReportSetOutputPayload {
static toCompletedReportPayload(
report: Report,
runMetadata?: RunMetadata
): ReportSetOutputPayload {
if (!report.id) {
throw new Error('Report ID is required to create completed report payload');
}
return {
id: parseInt(report.id, 10),
status: 'complete',
output: report.output ? convertReportOutputToJson(report.output as any) : null,
...runMetadata,
};
}

/**
* Creates payload for marking a report as errored
*/
static toErrorReportPayload(report: Report, errorMessage?: string): ReportSetOutputPayload {
static toErrorReportPayload(
report: Report,
errorMessage?: string,
runMetadata?: RunMetadata
): ReportSetOutputPayload {
if (!report.id) {
throw new Error('Report ID is required to create error report payload');
}
const payload: ReportSetOutputPayload = {
id: parseInt(report.id, 10),
status: 'error',
output: null,
...runMetadata,
};
if (errorMessage) {
payload.error_message = errorMessage;
Expand Down
19 changes: 16 additions & 3 deletions app/src/adapters/SimulationAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Simulation } from '@/types/ingredients/Simulation';
import { SimulationMetadata } from '@/types/metadata/simulationMetadata';
import { SimulationCreationPayload, SimulationSetOutputPayload } from '@/types/payloads';
import type { RunMetadata } from '@/types/runMetadata';

/**
* Adapter for converting between Simulation and API formats
Expand Down Expand Up @@ -111,35 +112,47 @@ export class SimulationAdapter {
static toUpdatePayload(
id: number,
output: unknown,
status: 'pending' | 'complete' | 'error'
status: 'pending' | 'complete' | 'error',
runMetadata?: RunMetadata
): SimulationSetOutputPayload {
return {
id,
output: typeof output === 'string' ? output : output ? JSON.stringify(output) : null,
status,
...runMetadata,
};
}

/**
* Creates payload for marking a simulation as completed with output
* Note: Output is NOT stringified here - it's stringified when the entire payload is JSON.stringified
*/
static toCompletedPayload(id: number, output: unknown): SimulationSetOutputPayload {
static toCompletedPayload(
id: number,
output: unknown,
runMetadata?: RunMetadata
): SimulationSetOutputPayload {
return {
id,
output: typeof output === 'string' ? output : JSON.stringify(output),
status: 'complete',
...runMetadata,
};
}

/**
* Creates payload for marking a simulation as errored
*/
static toErrorPayload(id: number, errorMessage?: string): SimulationSetOutputPayload {
static toErrorPayload(
id: number,
errorMessage?: string,
runMetadata?: RunMetadata
): SimulationSetOutputPayload {
const payload: SimulationSetOutputPayload = {
id,
output: null,
status: 'error',
...runMetadata,
};
if (errorMessage) {
payload.error_message = errorMessage;
Expand Down
15 changes: 11 additions & 4 deletions app/src/api/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Report } from '@/types/ingredients/Report';
import { UserReport } from '@/types/ingredients/UserReport';
import { ReportMetadata } from '@/types/metadata/reportMetadata';
import { ReportCreationPayload, ReportSetOutputPayload } from '@/types/payloads';
import type { RunMetadata } from '@/types/runMetadata';

export type CountryId = (typeof countryIds)[number];

Expand Down Expand Up @@ -105,19 +106,25 @@ async function updateReport(
export async function markReportCompleted(
countryId: (typeof countryIds)[number],
reportId: string,
report: Report
report: Report,
runMetadata?: RunMetadata
): Promise<ReportMetadata> {
const data = ReportAdapter.toCompletedReportPayload(report);
const data = ReportAdapter.toCompletedReportPayload(report, runMetadata);
return updateReport(countryId, reportId, data);
}

export async function markReportError(
countryId: (typeof countryIds)[number],
reportId: string,
report: Report,
errorMessage?: string
errorMessage?: string,
runMetadata?: RunMetadata
): Promise<ReportMetadata> {
const data = ReportAdapter.toErrorReportPayload(report, errorMessage);
const data = ReportAdapter.toErrorReportPayload(
report,
errorMessage,
runMetadata
);
return updateReport(countryId, reportId, data);
}

Expand Down
24 changes: 18 additions & 6 deletions app/src/api/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BASE_URL } from '@/constants';
import { countryIds } from '@/libs/countries';
import { SimulationMetadata } from '@/types/metadata/simulationMetadata';
import { SimulationCreationPayload } from '@/types/payloads';
import type { RunMetadata } from '@/types/runMetadata';

export async function fetchSimulationById(
countryId: (typeof countryIds)[number],
Expand Down Expand Up @@ -83,11 +84,16 @@ export async function createSimulation(
export async function updateSimulationOutput(
countryId: (typeof countryIds)[number],
simulationId: string,
output: unknown
output: unknown,
runMetadata?: RunMetadata
): Promise<SimulationMetadata> {
const url = `${BASE_URL}/${countryId}/simulation`;

const payload = SimulationAdapter.toCompletedPayload(parseInt(simulationId, 10), output);
const payload = SimulationAdapter.toCompletedPayload(
parseInt(simulationId, 10),
output,
runMetadata
);

const response = await fetch(url, {
method: 'PATCH',
Expand Down Expand Up @@ -120,9 +126,10 @@ export async function updateSimulationOutput(
export async function markSimulationCompleted(
countryId: (typeof countryIds)[number],
simulationId: string,
output: unknown
output: unknown,
runMetadata?: RunMetadata
): Promise<SimulationMetadata> {
return updateSimulationOutput(countryId, simulationId, output);
return updateSimulationOutput(countryId, simulationId, output, runMetadata);
}

/**
Expand All @@ -132,11 +139,16 @@ export async function markSimulationCompleted(
export async function markSimulationError(
countryId: (typeof countryIds)[number],
simulationId: string,
errorMessage?: string
errorMessage?: string,
runMetadata?: RunMetadata
): Promise<SimulationMetadata> {
const url = `${BASE_URL}/${countryId}/simulation`;

const payload = SimulationAdapter.toErrorPayload(parseInt(simulationId, 10), errorMessage);
const payload = SimulationAdapter.toErrorPayload(
parseInt(simulationId, 10),
errorMessage,
runMetadata
);

const response = await fetch(url, {
method: 'PATCH',
Expand Down
58 changes: 49 additions & 9 deletions app/src/libs/calculations/ResultPersister.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { QueryClient } from '@tanstack/react-query';
import { markReportCompleted } from '@/api/report';
import { updateSimulationOutput } from '@/api/simulation';
import { mergeConsistentRunMetadata } from '@/libs/calculations/runMetadata';
import { calculationKeys, reportKeys, simulationKeys } from '@/libs/queryKeys';
import type { CalcStatus } from '@/types/calculation';
import type { Report } from '@/types/ingredients/Report';
import type { RunMetadata } from '@/types/runMetadata';

/**
* Persists calculation results to the appropriate backend resource
Expand All @@ -26,13 +28,20 @@ export class ResultPersister {

try {
if (status.metadata.targetType === 'report') {
await this.persistToReport(status.metadata.calcId, status.result, countryId, year);
await this.persistToReport(
status.metadata.calcId,
status.result,
countryId,
year,
status.runMetadata
);
} else {
await this.persistToSimulation(
status.metadata.calcId,
status.result,
countryId,
status.metadata.reportId // Pass parent reportId for household sim-level calcs
status.metadata.reportId, // Pass parent reportId for household sim-level calcs
status.runMetadata
);
}
} catch (error) {
Expand All @@ -41,13 +50,20 @@ export class ResultPersister {
await this.sleep(1000);
try {
if (status.metadata.targetType === 'report') {
await this.persistToReport(status.metadata.calcId, status.result, countryId, year);
await this.persistToReport(
status.metadata.calcId,
status.result,
countryId,
year,
status.runMetadata
);
} else {
await this.persistToSimulation(
status.metadata.calcId,
status.result,
countryId,
status.metadata.reportId // Pass parent reportId for household sim-level calcs
status.metadata.reportId, // Pass parent reportId for household sim-level calcs
status.runMetadata
);
}
} catch (retryError) {
Expand All @@ -66,7 +82,8 @@ export class ResultPersister {
reportId: string,
result: any,
countryId: string,
year: string
year: string,
runMetadata?: RunMetadata
): Promise<void> {
// Create a Report object with the result
const report: Report = {
Expand All @@ -80,7 +97,7 @@ export class ResultPersister {
};

// Use existing markReportCompleted API
await markReportCompleted(countryId as any, reportId, report);
await markReportCompleted(countryId as any, reportId, report, runMetadata);

// Invalidate report metadata cache so Reports page shows updated status
// WHY: Reports page reads from reportKeys.byId(), not calculation cache.
Expand All @@ -101,10 +118,11 @@ export class ResultPersister {
simulationId: string,
result: any,
countryId: string,
reportId?: string
reportId?: string,
runMetadata?: RunMetadata
): Promise<void> {
// Use new updateSimulationOutput API
await updateSimulationOutput(countryId as any, simulationId, result);
await updateSimulationOutput(countryId as any, simulationId, result, runMetadata);

// Invalidate simulation metadata cache so Reports page shows updated status
// WHY: Reports page may display simulation info, and we need fresh data after persistence.
Expand All @@ -128,11 +146,33 @@ export class ResultPersister {
const aggregatedOutput = await this.aggregateSimulationOutputs(reportId);

// Mark report as complete with aggregated output
await this.persistToReport(reportId, aggregatedOutput, countryId, report.year);
await this.persistToReport(
reportId,
aggregatedOutput,
countryId,
report.year,
this.aggregateRunMetadata(reportId)
);
}
}
}

private aggregateRunMetadata(reportId: string): RunMetadata | undefined {
const report = this.queryClient.getQueryData<Report>(reportKeys.byId(reportId));
if (!report) {
return undefined;
}

const runMetadataItems = report.simulationIds.map((simId) => {
const simStatus = this.queryClient.getQueryData<CalcStatus>(
calculationKeys.bySimulationId(simId)
);
return simStatus?.runMetadata;
});

return mergeConsistentRunMetadata(runMetadataItems);
}

/**
* Check if all simulations for a report are complete
* @param reportId - Parent report ID
Expand Down
2 changes: 2 additions & 0 deletions app/src/libs/calculations/economy/SocietyWideCalcStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
SocietyWideCalculationParams,
SocietyWideCalculationResponse,
} from '@/api/societyWideCalculation';
import { buildRunMetadataFromSocietyWideOutput } from '@/libs/calculations/runMetadata';
import { getDurationForCountry } from '@/constants/calculationDurations';
import { CalcMetadata, CalcParams, CalcStatus } from '@/types/calculation';
import { CalcExecutionStrategy, RefetchConfig } from '../strategies/types';
Expand Down Expand Up @@ -105,6 +106,7 @@ export class SocietyWideCalcStrategy implements CalcExecutionStrategy {
status: 'complete',
result: response.result,
metadata,
runMetadata: buildRunMetadataFromSocietyWideOutput(response.result),
};
}

Expand Down
Loading
Loading