{"record":{"id":"37f5fb1a15df0244","repo":"koala73/worldmonitor","slug":"custom-scorecard-bloc-members-must-be-uppercase-is","errorCode":null,"errorMessage":"Custom scorecard bloc members must be uppercase ISO-2 codes.","messagePattern":"Custom scorecard bloc members must be uppercase ISO-2 codes\\.","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/scorecard/v1/_bloc-presets.ts","lineNumber":38,"sourceCode":"    id: 'NATO',\n    label: 'NATO',\n    members: ['AL', 'BE', 'BG', 'CA', 'HR', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IS', 'IT', 'LV', 'LT', 'LU', 'ME', 'NL', 'MK', 'NO', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'TR', 'GB', 'US'],\n  },\n};\n\nexport function resolveBlocSelection(input: { preset?: string; members?: string[] }): ScorecardBlocSelection {\n  const preset = String(input.preset || '').trim();\n  const members = Array.isArray(input.members) ? input.members : [];\n  if (Boolean(preset) === (members.length > 0)) {\n    throw new Error('Select exactly one scorecard bloc preset or custom member list.');\n  }\n  if (preset) {\n    if (!(preset in SCORECARD_BLOC_PRESETS)) throw new Error(`Unknown scorecard bloc preset: ${preset}`);\n    return SCORECARD_BLOC_PRESETS[preset as ScorecardBlocPreset];\n  }\n  if (members.length < 2 || members.length > 30) throw new Error('Custom scorecard blocs require 2-30 members.');\n  if (members.some((member) => !/^[A-Z]{2}$/.test(member))) {\n    throw new Error('Custom scorecard bloc members must be uppercase ISO-2 codes.');\n  }\n  if (new Set(members).size !== members.length) throw new Error('Custom scorecard bloc members must be unique.');\n  if (members.some((member) => !isInRankableUniverse(member))) {\n    throw new Error('Custom scorecard bloc members must belong to the public rankable country universe.');\n  }\n  const sorted = [...members].sort();\n  return { id: `custom:${sorted.join('-')}`, label: sorted.join(' + '), members: sorted };\n}\n","sourceCodeStart":20,"sourceCodeEnd":47,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/server/worldmonitor/scorecard/v1/_bloc-presets.ts#L20-L47","documentation":"Each custom member in resolveBlocSelection must match /^[A-Z]{2}$/ — exactly two uppercase ASCII letters (ISO 3166-1 alpha-2). Lowercase codes, 3-letter ISO-3 codes, names, whitespace, or mixed-case entries cause this error. The strict format guarantees deterministic, safe cache keys (custom:<sorted codes joined>) and stable sorting.","triggerScenarios":"Calling getBlocScorecardWithReader with members like ['de','FR'] (lowercase), ['USA','FR'] (ISO-3), ['Germany','FR'] (names), ['D E','FR'], or ['US ','FR'] (trailing space — trim yourself first).","commonSituations":"1) Client collects country names from free-text input instead of an ISO-2 picker; 2) codes arrive lowercase from a locale/URL slug; 3) server previously accepted case-insensitive input and an old client still sends lowercase; 4) CSV/clipboard paste introduces stray whitespace.","solutions":["Uppercase and trim each member before sending: m.trim().toUpperCase().","Validate with /^[A-Z]{2}$/ client-side and reject or map non-conforming entries (names/ISO-3 → ISO-2 lookup).","Use a dropdown of valid ISO-2 codes instead of free-text input.","If a code can't be mapped, drop it (if ≥2 remain) rather than sending an invalid entry."],"exampleFix":"// before\nresolveBlocSelection({ members: ['de', 'fr'] }); // throws\n// after\nconst members = raw.map((m) => m.trim().toUpperCase()).filter((m) => /^[A-Z]{2}$/.test(m));\nif (members.length >= 2) resolveBlocSelection({ members });","handlingStrategy":"type-guard","validationCode":"const members = raw.map((m) => String(m).trim().toUpperCase());\nif (!members.every((m) => /^[A-Z]{2}$/.test(m))) throw new Error('members must be uppercase ISO-2 codes');","typeGuard":"function isUppercaseIso2(v: unknown): v is string {\n  return typeof v === 'string' && /^[A-Z]{2}$/.test(v);\n}","tryCatchPattern":"try {\n  const bloc = await getBlocScorecard({ members });\n} catch (err) {\n  if (err.message.includes('uppercase ISO-2 codes')) {\n    return badRequest({ invalid: members.filter((m) => !/^[A-Z]{2}$/.test(m)) });\n  }\n  throw err;\n}","preventionTips":["Normalize (trim/uppercase) at input collection time","Use an ISO-2 dropdown instead of free text","Provide a name/ISO-3 → ISO-2 lookup for pasted data","Type member arrays as UppercaseIso2 in TypeScript"],"tags":["validation","iso3166","scorecard","format"],"backgroundTag":"invalid-country-code","analyzedSha":"9361220cc013571781071f0206e4d80fd14b2f7f","analyzedAt":"2026-09-01T10:32:37.851Z","contentChangedAt":"2026-09-01T10:32:37.851Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}