diff --git a/frontend/components/dashboard/scan-terminal/__tests__/marketScanState.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/marketScanState.test.ts index b10f394f..3a47898b 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/marketScanState.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/marketScanState.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { buildCityMarketScanCacheKey, deriveCityMarketScanView, + normalizeCityMarketScanPayload, resolveCityMarketScanSnapshot, writeCachedCityMarketScan, } from "@/components/dashboard/scan-terminal/market-scan-state"; @@ -74,6 +75,35 @@ export function runTests() { assert.equal((cachedSnapshot.payload as unknown as { label: string }).label, "cached"); } + storage.clear(); + writeCachedCityMarketScan(cacheKey, { + market_scan: { + available: true, + market_price: 0.41, + selected_slug: "highest-temperature-in-nyc-on-april-30-2026-60-61f", + }, + selected_date: "2026-04-30", + } as unknown as MarketScan); + const wrappedCachedSnapshot = resolveCityMarketScanSnapshot({ + detail: cityDetail(), + detailCityName: "Test City", + enabled: true, + }); + assert.equal(wrappedCachedSnapshot.action, "success"); + if (wrappedCachedSnapshot.action === "success") { + assert.equal(wrappedCachedSnapshot.payload.available, true); + assert.equal(wrappedCachedSnapshot.payload.market_price, 0.41); + assert.equal(wrappedCachedSnapshot.payload.selected_date, "2026-04-30"); + } + + const wrappedResponse = normalizeCityMarketScanPayload({ + market_scan: { available: true, yes_buy: 0.42 } as MarketScan, + selected_date: "2026-04-30", + }); + assert.equal(wrappedResponse?.available, true); + assert.equal(wrappedResponse?.yes_buy, 0.42); + assert.equal(wrappedResponse?.selected_date, "2026-04-30"); + storage.clear(); assert.equal( resolveCityMarketScanSnapshot({ diff --git a/frontend/components/dashboard/scan-terminal/market-scan-state.ts b/frontend/components/dashboard/scan-terminal/market-scan-state.ts index ec6369a6..2f3fe5d4 100644 --- a/frontend/components/dashboard/scan-terminal/market-scan-state.ts +++ b/frontend/components/dashboard/scan-terminal/market-scan-state.ts @@ -7,11 +7,19 @@ import type { RemoteData } from "@/components/dashboard/scan-terminal/scan-termi import type { CityDetail, MarketScan } from "@/lib/dashboard-types"; import { normalizeCityKey } from "./decision-utils"; -const CITY_MARKET_SCAN_CACHE_PREFIX = "polyWeather_cityMarketScan_v3"; +const CITY_MARKET_SCAN_CACHE_PREFIX = "polyWeather_cityMarketScan_v4"; const CITY_MARKET_SCAN_CACHE_TTL_MS = 10 * 60 * 1000; export type CityMarketScanStatus = "idle" | "loading" | "ready" | "failed"; +export type CityMarketScanApiPayload = + | MarketScan + | { + fetched_at?: string | null; + market_scan?: MarketScan | null; + selected_date?: string | null; + }; + export type CityMarketScanSnapshot = | { action: "reset"; cacheKey?: string } | { action: "success"; cacheKey: string; payload: MarketScan; shouldWriteCache?: boolean } @@ -32,13 +40,30 @@ export function buildCityMarketScanCacheKey({ } export function readCachedCityMarketScan(cacheKey: string) { - return readCachedPayload(cacheKey, CITY_MARKET_SCAN_CACHE_TTL_MS); + return normalizeCityMarketScanPayload( + readCachedPayload(cacheKey, CITY_MARKET_SCAN_CACHE_TTL_MS), + ); } export function writeCachedCityMarketScan(cacheKey: string, payload: MarketScan) { writeCachedPayload(cacheKey, payload); } +export function normalizeCityMarketScanPayload( + payload: CityMarketScanApiPayload | null | undefined, +): MarketScan | null { + if (!payload || typeof payload !== "object") return null; + if ("market_scan" in payload) { + const scan = payload.market_scan; + if (!scan || typeof scan !== "object") return null; + return { + ...scan, + selected_date: scan.selected_date ?? payload.selected_date ?? null, + }; + } + return payload as MarketScan; +} + export function resolveCityMarketScanSnapshot({ detail, detailCityName, diff --git a/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts b/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts index 9e74a5da..97186e3b 100644 --- a/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts +++ b/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts @@ -1,6 +1,10 @@ "use client"; import { extractStreamingAirportRead } from "@/components/dashboard/scan-terminal/ai-city-stream"; +import { + normalizeCityMarketScanPayload, + type CityMarketScanApiPayload, +} from "@/components/dashboard/scan-terminal/market-scan-state"; import type { AiCityForecastPayload } from "@/components/dashboard/scan-terminal/types"; import { buildBrowserBackendHeaders, @@ -292,7 +296,7 @@ async function getMarketScan(city: string, options: MarketScanQueryOptions = {}) if (options.marketSlug) params.set("market_slug", options.marketSlug); if (options.lite != null) params.set("lite", String(options.lite)); const headers = await buildBrowserBackendHeaders({ Accept: "application/json" }); - return readJsonOrThrow( + const payload = await readJsonOrThrow( `/api/city/${encodeURIComponent(city)}/market-scan?${params.toString()}`, { cache: "no-store", @@ -300,6 +304,12 @@ async function getMarketScan(city: string, options: MarketScanQueryOptions = {}) signal: options.signal, }, ); + return ( + normalizeCityMarketScanPayload(payload) ?? { + available: false, + reason: "No market scan payload returned.", + } + ); } function runQueuedAiCityReadTask(task: () => Promise, onQueued?: () => void) {