import { DEFAULT_BACKEND_METRICS_BASE_URL } from "./backend-metrics-config"; export interface BackendMetricsRow { a3IncreaseCount: string; afterViewSearchCount: string; afterViewSearchRate: string; cpSearch: string; cpa3: string; newA3Rate: string; starId: string; } interface FetchResponseLike { json(): Promise; ok: boolean; } type FetchLike = ( input: string, init?: RequestInit ) => Promise; interface BackendMetricsClientOptions { baseUrl?: string; fetchImpl?: FetchLike; getAccessToken: () => Promise; } export function createBackendMetricsClient(options: BackendMetricsClientOptions) { const baseUrl = options.baseUrl ?? DEFAULT_BACKEND_METRICS_BASE_URL; const fetchImpl = options.fetchImpl ?? defaultFetch; return { async searchByStarIds(starIds: string[]): Promise { const response = await fetchImpl(buildBackendMetricsSearchUrl(baseUrl), { body: JSON.stringify(buildBackendMetricsSearchRequestBody(starIds)), headers: { Authorization: `Bearer ${await options.getAccessToken()}`, "Content-Type": "application/json" }, method: "POST" }); if (!response.ok) { throw new Error("backend metrics request failed"); } return mapBackendMetricsSearchResponse(await response.json()); } }; } export function buildBackendMetricsSearchUrl(baseUrl: string): string { return new URL("/api/v1/history/talents/search", baseUrl).toString(); } export function buildBackendMetricsSearchRequestBody(starIds: string[]) { return { page: 1, size: Math.max(20, starIds.length), type: "star_id", values: starIds }; } export function mapBackendMetricsSearchResponse(payload: unknown): BackendMetricsRow[] { const rows = readResponseRows(payload); if (!rows) { throw new Error("backend metrics response is invalid"); } return rows.flatMap((row) => { if (!isRecord(row) || typeof row.star_id !== "string") { return []; } return [ { a3IncreaseCount: formatDecimalValue(row.avg_a3_increase_cnt), afterViewSearchCount: formatDecimalValue(row.avg_after_view_search_cnt), afterViewSearchRate: formatRateValue(row.avg_after_view_search_rate), cpSearch: formatDecimalValue(row.cp_search), cpa3: formatDecimalValue(row.cpa3), newA3Rate: formatRateValue(row.avg_new_a3_rate), starId: row.star_id } ]; }); } function readResponseRows(payload: unknown): unknown[] | null { if (!isRecord(payload) || payload.success !== true) { return null; } const topLevelData = isRecord(payload.data) ? payload.data : null; return Array.isArray(topLevelData?.data) ? topLevelData.data : null; } function formatRateValue(value: unknown): string { const number = typeof value === "number" ? value : Number(value); if (Number.isFinite(number)) { const percentage = number * 100; const formatted = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2, minimumFractionDigits: percentage % 1 === 0 ? 0 : 2 }).format(percentage); return `${formatted}%`; } return ""; } function formatDecimalValue(value: unknown): string { const number = typeof value === "number" ? value : Number(value); if (!Number.isFinite(number)) { return ""; } return new Intl.NumberFormat("en-US", { maximumFractionDigits: 2, minimumFractionDigits: 2 }).format(number); } async function defaultFetch(input: string, init?: RequestInit) { return fetch(input, init); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; }