{"record":{"id":"c4a33716cd64fa16","repo":"koala73/worldmonitor","slug":"unknown-chokepointid-chokepointid","errorCode":null,"errorMessage":"Unknown chokepointId: ${chokepointId}","messagePattern":"Unknown chokepointId: (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/supply-chain/v1/get-multi-sector-cost-shock.ts","lineNumber":86,"sourceCode":"  req: GetMultiSectorCostShockRequest,\n): Promise<GetMultiSectorCostShockResponse> {\n  const iso2 = (req.iso2 ?? '').trim().toUpperCase();\n  const chokepointId = (req.chokepointId ?? '').trim().toLowerCase();\n  const closureDays = clampClosureDays(req.closureDays ?? 30);\n\n  // Input-shape errors return 400 — restoring the legacy /api/supply-chain/v1/\n  // multi-sector-cost-shock contract. Empty-payload-200 is reserved for the\n  // PRO-gate deny path (intentional contract shift), not for caller bugs\n  // (malformed or missing fields). Distinguishing the two matters for external\n  // API consumers, tests, and silent-failure detection in logs.\n  if (!/^[A-Z]{2}$/.test(iso2)) {\n    throw new ValidationError([{ field: 'iso2', description: 'iso2 must be a 2-letter uppercase ISO country code' }]);\n  }\n  if (!chokepointId) {\n    throw new ValidationError([{ field: 'chokepointId', description: 'chokepointId is required' }]);\n  }\n  if (!CHOKEPOINT_REGISTRY.some(c => c.id === chokepointId)) {\n    throw new ValidationError([{ field: 'chokepointId', description: `Unknown chokepointId: ${chokepointId}` }]);\n  }\n\n  const isPro = await isCallerPremium(ctx.request);\n  if (!isPro) return emptyResponse(iso2, chokepointId, closureDays);\n\n  // Seeder writes the products payload via raw key (no env-prefix) — read raw.\n  const productsKey = `comtrade:bilateral-hs4:${iso2}:v1`;\n  const [productsCache, statusCache] = await Promise.all([\n    getCachedJson(productsKey, true).catch(() => null) as Promise<CountryProductsCache | null>,\n    getCachedJson(CHOKEPOINT_STATUS_KEY).catch(() => null) as Promise<{ chokepoints?: ChokepointInfo[] } | null>,\n  ]);\n\n  const products = Array.isArray(productsCache?.products) ? productsCache.products : [];\n  const importsByHs2 = aggregateAnnualImportsByHs2(products);\n  const hasAnyImports = Object.values(importsByHs2).some(v => v > 0);\n  const warRiskTier = (statusCache?.chokepoints?.find(c => c.id === chokepointId)?.warRiskTier\n    ?? 'WAR_RISK_TIER_NORMAL') as WarRiskTier;\n","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/supply-chain/v1/get-multi-sector-cost-shock.ts#L68-L104","documentation":"Validation error in getMultiSectorCostShock: a chokepointId was supplied (and lowercased) but does not match any chokepoint known to the model. Like other input-shape errors it returns 400 to preserve the legacy external API contract, separating caller bugs from the intentional PRO-gate empty-200 deny path.","triggerScenarios":"Thrown at server/worldmonitor/supply-chain/v1/get-multi-sector-cost-shock.ts:86 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Use a supported chokepointId from the supply-chain chokepoint reference data","Verify spelling: IDs are compared after lowercasing, so mixed case is fine but unknown names are rejected"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}