mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-27 22:45:03 +00:00
fix(reporting): handle string timestamp from raw db.execute in scenarios
getReportingScenarios uses raw db.execute, which returns created_at as a string (Drizzle .select() returns a Date). Calling .toISOString() on it threw 'created_at.toISOString is not a function' and 500'd /portfolio/[id]/reporting. Extracted the row→ReportingScenario mapping into a pure, tested module (reportingScenarios.ts, toReportingScenario) that normalises created_at whether it arrives as a string or a Date, and covers the modelled-status and bigint-id / null-name cases. databaseFunctions re-exports the type. TDD: 4 tests; full suite 446 green, tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
97e24e0027
commit
9d8395f3ac
3 changed files with 103 additions and 22 deletions
|
|
@ -24,6 +24,12 @@ import type {
|
|||
BaselineMetrics,
|
||||
TotalMetrics,
|
||||
} from "./types";
|
||||
import {
|
||||
toReportingScenario,
|
||||
type ReportingScenario,
|
||||
} from "./reportingScenarios";
|
||||
|
||||
export type { ReportingScenario };
|
||||
|
||||
export async function getPortfolioCounts(portfolioId: number): Promise<number> {
|
||||
const result = await db.execute<{ total: number }>(sql`
|
||||
|
|
@ -239,17 +245,6 @@ export async function loadBaselineMetrics(
|
|||
};
|
||||
}
|
||||
|
||||
export interface ReportingScenario {
|
||||
id: number;
|
||||
name: string;
|
||||
goal: string;
|
||||
goalValue: string | null;
|
||||
budget: number | null;
|
||||
housingType: string;
|
||||
createdAt: string;
|
||||
status: "modelled" | "awaiting_modelling";
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenarios for the reporting combobox: config plus derived modelled state
|
||||
* (ADR-0003). Deliberately lighter than listScenariosWithStatus — it derives
|
||||
|
|
@ -271,7 +266,7 @@ export async function getReportingScenarios(
|
|||
goal_value: string | null;
|
||||
budget: number | null;
|
||||
housing_type: string;
|
||||
created_at: Date;
|
||||
created_at: string | Date;
|
||||
}>(sql`
|
||||
SELECT id, name, goal, goal_value, budget, housing_type, created_at
|
||||
FROM scenario
|
||||
|
|
@ -287,16 +282,7 @@ export async function getReportingScenarios(
|
|||
|
||||
const modelled = new Set(modelledRes.rows.map((r) => String(r.scenario_id)));
|
||||
|
||||
return configRes.rows.map((s) => ({
|
||||
id: Number(s.id),
|
||||
name: s.name ?? `Scenario ${s.id}`,
|
||||
goal: s.goal,
|
||||
goalValue: s.goal_value,
|
||||
budget: s.budget,
|
||||
housingType: s.housing_type,
|
||||
createdAt: s.created_at.toISOString(),
|
||||
status: modelled.has(String(s.id)) ? "modelled" : "awaiting_modelling",
|
||||
}));
|
||||
return configRes.rows.map((s) => toReportingScenario(s, modelled));
|
||||
}
|
||||
|
||||
/** The portfolio's own goal — drives the current-stock goal callout. */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { toReportingScenario } from "./reportingScenarios";
|
||||
|
||||
describe("toReportingScenario", () => {
|
||||
const baseRow = {
|
||||
id: "12",
|
||||
name: "EPC C by 2030",
|
||||
goal: "Increasing EPC",
|
||||
goal_value: "C",
|
||||
budget: 15000,
|
||||
housing_type: "Social",
|
||||
created_at: "2026-07-04T09:10:00.000Z",
|
||||
};
|
||||
|
||||
it("normalises a string timestamp from raw db.execute to an ISO string", () => {
|
||||
// Regression: raw db.execute returns created_at as a string, so calling
|
||||
// .toISOString() on it threw. The mapper must accept a string.
|
||||
const s = toReportingScenario(baseRow, new Set(["12"]));
|
||||
expect(s.createdAt).toBe("2026-07-04T09:10:00.000Z");
|
||||
});
|
||||
|
||||
it("also accepts a Date timestamp", () => {
|
||||
const s = toReportingScenario(
|
||||
{ ...baseRow, created_at: new Date("2026-07-04T09:10:00.000Z") },
|
||||
new Set(),
|
||||
);
|
||||
expect(s.createdAt).toBe("2026-07-04T09:10:00.000Z");
|
||||
});
|
||||
|
||||
it("marks a scenario modelled only when its id is in the modelled set", () => {
|
||||
expect(toReportingScenario(baseRow, new Set(["12"])).status).toBe(
|
||||
"modelled",
|
||||
);
|
||||
expect(toReportingScenario(baseRow, new Set(["99"])).status).toBe(
|
||||
"awaiting_modelling",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to a placeholder name and coerces a bigint id", () => {
|
||||
const s = toReportingScenario(
|
||||
{ ...baseRow, id: 12n as unknown as bigint, name: null },
|
||||
new Set(["12"]),
|
||||
);
|
||||
expect(s.id).toBe(12);
|
||||
expect(s.name).toBe("Scenario 12");
|
||||
expect(s.status).toBe("modelled");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Pure mapping for the reporting scenario combobox. Lives apart from the db
|
||||
* function so the row-shaping (including the string|Date timestamp quirk of
|
||||
* raw db.execute) is unit-tested without a database.
|
||||
*/
|
||||
|
||||
export interface ReportingScenario {
|
||||
id: number;
|
||||
name: string;
|
||||
goal: string;
|
||||
goalValue: string | null;
|
||||
budget: number | null;
|
||||
housingType: string;
|
||||
createdAt: string;
|
||||
status: "modelled" | "awaiting_modelling";
|
||||
}
|
||||
|
||||
export interface RawScenarioRow {
|
||||
id: string | number | bigint;
|
||||
name: string | null;
|
||||
goal: string;
|
||||
goal_value: string | null;
|
||||
budget: number | null;
|
||||
housing_type: string;
|
||||
created_at: string | Date;
|
||||
}
|
||||
|
||||
/** Raw db.execute returns created_at as a string; Drizzle .select() as a Date. */
|
||||
function toIso(value: string | Date): string {
|
||||
return value instanceof Date ? value.toISOString() : value;
|
||||
}
|
||||
|
||||
export function toReportingScenario(
|
||||
row: RawScenarioRow,
|
||||
modelledIds: Set<string>,
|
||||
): ReportingScenario {
|
||||
return {
|
||||
id: Number(row.id),
|
||||
name: row.name ?? `Scenario ${row.id}`,
|
||||
goal: row.goal,
|
||||
goalValue: row.goal_value,
|
||||
budget: row.budget,
|
||||
housingType: row.housing_type,
|
||||
createdAt: toIso(row.created_at),
|
||||
status: modelledIds.has(String(row.id)) ? "modelled" : "awaiting_modelling",
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue