The natal /api/chart request from the chart page now sends skip_vedastro_main_entry_overview, matching the ephemeris page. Mapper and contract never read that evidence. BUG-718 recurs from BUG-161. Tests lock both foreground natal paths.
179 lines
4.9 KiB
TypeScript
179 lines
4.9 KiB
TypeScript
import { resolveAyanamsa } from "./ayanamsa.ts";
|
|
import {
|
|
chartViewMessageSchema,
|
|
type ChartViewResponse,
|
|
} from "./chart-view-contract.ts";
|
|
import {
|
|
enginePayload,
|
|
logEngineCallFailure,
|
|
type ChartViewLayer,
|
|
type EngineCallResult,
|
|
} from "./chart-view-engine.ts";
|
|
import { CHART_VIEW_COPY, VARGA_CHIP_ORDER } from "./chart-view-labels.ts";
|
|
import {
|
|
buildChartView,
|
|
engineChartHasPlanets,
|
|
type ChartViewProfileInput,
|
|
} from "./chart-view-mapper.ts";
|
|
|
|
export type ChartViewEnginePost = (
|
|
path: string,
|
|
body: Record<string, unknown>,
|
|
) => Promise<EngineCallResult>;
|
|
|
|
export type ChartViewLoadInput = {
|
|
userId: string | null;
|
|
profile: ChartViewProfileInput | null;
|
|
postEngine: ChartViewEnginePost;
|
|
asOf: string;
|
|
rateLimited?: boolean;
|
|
layers?: readonly ChartViewLayer[];
|
|
};
|
|
|
|
function message(
|
|
status: "unauthenticated" | "birth_profile_incomplete" | "chart_unavailable" | "rate_limited",
|
|
copy: string,
|
|
): ChartViewResponse {
|
|
return chartViewMessageSchema.parse({ status, billed: false, message: copy });
|
|
}
|
|
|
|
function birthPayload(profile: ChartViewProfileInput): Record<string, unknown> {
|
|
const [year, month, day] = profile.date.split("-").map(Number);
|
|
const [hour, minute] = profile.time.split(":").map(Number);
|
|
return {
|
|
year,
|
|
month,
|
|
day,
|
|
hour,
|
|
minute,
|
|
second: 0,
|
|
lat: profile.latitude,
|
|
lon: profile.longitude,
|
|
tz: profile.timezoneOffset,
|
|
latitude: profile.latitude,
|
|
longitude: profile.longitude,
|
|
timezone: profile.timezoneOffset,
|
|
ayanamsa: resolveAyanamsa({ ayanamsa: profile.ayanamsa }),
|
|
node_mode: "mean",
|
|
house_system: "P",
|
|
skip_vedastro_main_entry_overview: true,
|
|
};
|
|
}
|
|
|
|
function wantedLayers(layers: readonly ChartViewLayer[] | undefined): Set<ChartViewLayer> {
|
|
return new Set(layers ?? []);
|
|
}
|
|
|
|
async function callEngine(
|
|
postEngine: ChartViewEnginePost,
|
|
path: string,
|
|
body: Record<string, unknown>,
|
|
): Promise<EngineCallResult> {
|
|
const result = await postEngine(path, body);
|
|
if (result.status !== "ok") logEngineCallFailure(result);
|
|
return result;
|
|
}
|
|
|
|
export async function assembleChartView(input: ChartViewLoadInput): Promise<{
|
|
httpStatus: number;
|
|
body: ChartViewResponse;
|
|
}> {
|
|
if (!input.userId) {
|
|
return {
|
|
httpStatus: 401,
|
|
body: message("unauthenticated", CHART_VIEW_COPY.unauthenticated),
|
|
};
|
|
}
|
|
if (input.rateLimited) {
|
|
return {
|
|
httpStatus: 429,
|
|
body: message("rate_limited", CHART_VIEW_COPY.rateLimited),
|
|
};
|
|
}
|
|
const profile = input.profile;
|
|
if (!profile) {
|
|
return {
|
|
httpStatus: 200,
|
|
body: message("birth_profile_incomplete", CHART_VIEW_COPY.incomplete),
|
|
};
|
|
}
|
|
|
|
const payload = birthPayload(profile);
|
|
const chartResult = await callEngine(input.postEngine, "/api/chart", payload);
|
|
if (chartResult.status !== "ok") {
|
|
return {
|
|
httpStatus: 200,
|
|
body: message(
|
|
"chart_unavailable",
|
|
chartResult.status === "busy" ? CHART_VIEW_COPY.busy : CHART_VIEW_COPY.unavailable,
|
|
),
|
|
};
|
|
}
|
|
const chart = chartResult.payload;
|
|
if (chart.success === false || !engineChartHasPlanets(chart)) {
|
|
logEngineCallFailure({
|
|
status: "bad_payload",
|
|
path: "/api/chart",
|
|
elapsedMs: 0,
|
|
errorName: "engine_rejected",
|
|
});
|
|
return {
|
|
httpStatus: 200,
|
|
body: message("chart_unavailable", CHART_VIEW_COPY.unavailable),
|
|
};
|
|
}
|
|
|
|
const planets = chart.planets;
|
|
const ascendant = chart.ascendant;
|
|
const houses = chart.houses;
|
|
const followUp = {
|
|
...payload,
|
|
planets,
|
|
ascendant,
|
|
houses,
|
|
};
|
|
const layers = wantedLayers(input.layers);
|
|
const [varga, chara, western, qizheng] = await Promise.all([
|
|
layers.has("varga")
|
|
? callEngine(input.postEngine, "/api/varga_full", {
|
|
...followUp,
|
|
divisions: VARGA_CHIP_ORDER.map((division) => `D${division}`),
|
|
}).then(enginePayload)
|
|
: Promise.resolve(null),
|
|
layers.has("chara")
|
|
? callEngine(input.postEngine, "/api/dasha/chara", {
|
|
...followUp,
|
|
antardasha: true,
|
|
mode: "dasha",
|
|
}).then(enginePayload)
|
|
: Promise.resolve(null),
|
|
layers.has("western")
|
|
? callEngine(input.postEngine, "/api/western", payload).then(enginePayload)
|
|
: Promise.resolve(null),
|
|
layers.has("qizheng")
|
|
? callEngine(input.postEngine, "/api/qizheng", payload).then(enginePayload)
|
|
: Promise.resolve(null),
|
|
]);
|
|
|
|
try {
|
|
const body = buildChartView({
|
|
chart,
|
|
varga,
|
|
chara,
|
|
western,
|
|
qizheng,
|
|
profile,
|
|
asOf: input.asOf,
|
|
});
|
|
return { httpStatus: 200, body };
|
|
} catch (error) {
|
|
const errorName = error instanceof Error ? error.name : "Error";
|
|
const errorMessage = error instanceof Error ? error.message : "unknown";
|
|
console.warn("chart_view_assemble_failed", { errorName, message: errorMessage });
|
|
return {
|
|
httpStatus: 200,
|
|
body: message("chart_unavailable", CHART_VIEW_COPY.unavailable),
|
|
};
|
|
}
|
|
}
|