From ca8f53959b7c5919cb656392dff23291f070c812 Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Sun, 12 Apr 2026 19:41:27 +0400 Subject: [PATCH] fix(gold-intelligence): read correct Redis key and data shape (#3013) * fix(gold-intelligence): read correct Redis key and data shape Handler read 'market:commodity-quotes:v1' (doesn't exist). Actual seeded key is 'market:commodities-bootstrap:v1'. Also expected raw array but seeder writes { quotes: [...] }. Both bugs caused permanent "Gold data unavailable" in the panel. * fix(market): return unavailable when GC=F quote is missing from commodity snapshot --- .../market/v1/get-gold-intelligence.ts | 12 +++++++---- tests/gold-intelligence.test.mjs | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/server/worldmonitor/market/v1/get-gold-intelligence.ts b/server/worldmonitor/market/v1/get-gold-intelligence.ts index fcddf1fbd..64121436f 100644 --- a/server/worldmonitor/market/v1/get-gold-intelligence.ts +++ b/server/worldmonitor/market/v1/get-gold-intelligence.ts @@ -7,7 +7,7 @@ import type { } from '../../../../src/generated/server/worldmonitor/market/v1/service_server'; import { getCachedJson } from '../../../_shared/redis'; -const COMMODITY_KEY = 'market:commodity-quotes:v1'; +const COMMODITY_KEY = 'market:commodities-bootstrap:v1'; const COT_KEY = 'market:cot:v1'; interface RawQuote { @@ -44,18 +44,22 @@ export async function getGoldIntelligence( _req: GetGoldIntelligenceRequest, ): Promise { try { - const [rawQuotes, rawCot] = await Promise.all([ - getCachedJson(COMMODITY_KEY, true) as Promise, + const [rawPayload, rawCot] = await Promise.all([ + getCachedJson(COMMODITY_KEY, true) as Promise<{ quotes?: RawQuote[] } | null>, getCachedJson(COT_KEY, true) as Promise<{ instruments?: RawCotInstrument[]; reportDate?: string } | null>, ]); - if (!rawQuotes || !Array.isArray(rawQuotes)) { + const rawQuotes = rawPayload?.quotes; + if (!rawQuotes || !Array.isArray(rawQuotes) || rawQuotes.length === 0) { return { goldPrice: 0, goldChangePct: 0, goldSparkline: [], silverPrice: 0, platinumPrice: 0, palladiumPrice: 0, crossCurrencyPrices: [], updatedAt: '', unavailable: true }; } const quoteMap = new Map(rawQuotes.map(q => [q.symbol, q])); const gold = quoteMap.get('GC=F'); + if (!gold) { + return { goldPrice: 0, goldChangePct: 0, goldSparkline: [], silverPrice: 0, platinumPrice: 0, palladiumPrice: 0, crossCurrencyPrices: [], updatedAt: '', unavailable: true }; + } const silver = quoteMap.get('SI=F'); const platinum = quoteMap.get('PL=F'); const palladium = quoteMap.get('PA=F'); diff --git a/tests/gold-intelligence.test.mjs b/tests/gold-intelligence.test.mjs index 1f0ebed21..6ac896bef 100644 --- a/tests/gold-intelligence.test.mjs +++ b/tests/gold-intelligence.test.mjs @@ -97,6 +97,26 @@ describe('Gold Intelligence', () => { assert.ok(Math.abs(premium - ((3200 - 950) / 950) * 100) < 0.01); }); + it('returns unavailable when GC=F is missing from commodity snapshot', () => { + const quotes = [ + { symbol: 'SI=F', price: 35 }, + { symbol: 'PL=F', price: 950 }, + { symbol: 'PA=F', price: 1020 }, + { symbol: 'EURUSD=X', price: 1.08 }, + ]; + const quoteMap = new Map(quotes.map(q => [q.symbol, q])); + const gold = quoteMap.get('GC=F'); + assert.strictEqual(gold, undefined); + + const goldPrice = gold?.price ?? 0; + assert.strictEqual(goldPrice, 0); + + const ratio = computeGoldSilverRatio(goldPrice, 35); + assert.strictEqual(ratio, null); + const cross = computeCrossCurrency(goldPrice, quotes); + assert.strictEqual(cross.length, 0); + }); + it('partial availability: price works when cot is null, and vice versa', () => { const goldPrice = 3200; const silverPrice = 35;