{"record":{"id":"cf9449cbceee2f4c","repo":"ZhuLinsen/daily_stock_analysis","slug":"error-cf9449","errorCode":null,"errorMessage":"请提供文件或粘贴文本","messagePattern":"请提供文件或粘贴文本","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/dsa-web/src/api/stocks.ts","lineNumber":52,"sourceCode":"      rawText: data.raw_text,\n    };\n  },\n\n  async parseImport(file?: File, text?: string): Promise<ExtractFromImageResponse> {\n    if (file) {\n      const formData = new FormData();\n      formData.append('file', file);\n      const headers: { [key: string]: string | undefined } = { 'Content-Type': undefined };\n      const response = await apiClient.post('/api/v1/stocks/parse-import', formData, { headers });\n      const data = response.data as { codes?: string[]; items?: ExtractItem[] };\n      return { codes: data.codes ?? [], items: data.items };\n    }\n    if (text) {\n      const response = await apiClient.post('/api/v1/stocks/parse-import', { text });\n      const data = response.data as { codes?: string[]; items?: ExtractItem[] };\n      return { codes: data.codes ?? [], items: data.items };\n    }\n    throw new Error('请提供文件或粘贴文本');\n  },\n};\n","sourceCodeStart":34,"sourceCodeEnd":55,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/apps/dsa-web/src/api/stocks.ts#L34-L55","documentation":"Raised during full account replay (_replay_account, portfolio_service.py:819) when a cash event's direction is neither 'in' nor 'out'. The replay must decide whether to add or subtract the amount; an unknown direction aborts the entire snapshot computation with validation_error.","triggerScenarios":"A cash_events row with direction='IN ' with trailing characters that break the exact match (note: unlike the query path, this check is exact, not lowercased), direction='deposit', direction=NULL, or any non-enum value written outside add_cash_event's validated path.","commonSituations":"Direct DB inserts from migration scripts using debit/credit vocabulary; importer defaulting missing direction to NULL; a legacy writer that stored 'IN'/'OUT' uppercase before normalization was added at the write boundary — this replay branch compares event.direction == 'in' exactly.","solutions":["Query cash_events for the account where direction IS NULL or NOT IN ('in','out') and repair the rows","Normalize legacy rows: UPDATE cash_events SET direction = LOWER(TRIM(direction)) and map synonyms to in/out","Route all new writes through add_cash_event, which validates against VALID_CASH_DIRECTIONS","Add an audit query after bulk imports"],"exampleFix":"# before\nraw_insert(direction=\"DEPOSIT\", amount=1000.0)\n# after\nsvc.add_cash_event(account_id=1, direction=\"in\", amount=1000.0, event_date=d, ...)","handlingStrategy":"validation","validationCode":"VALID_CASH_DIRECTIONS = {\"in\", \"out\"}\n\ndef stored_direction_ok(direction):\n    return bool(direction) and direction.strip().lower() in VALID_CASH_DIRECTIONS","typeGuard":"from typing import Literal, TypeGuard\n\nStoredCashDirection = Literal[\"in\", \"out\"]\n\ndef is_stored_cash_direction(value: str | None) -> TypeGuard[StoredCashDirection]:\n    return bool(value) and value.strip().lower() in {\"in\", \"out\"}","tryCatchPattern":"try:\n    svc.get_snapshot(account_id=a)\nexcept ValueError as exc:\n    if \"Unsupported cash direction\" in str(exc):\n        normalize_cash_directions_in_db(a); svc.get_snapshot(account_id=a)\n    else:\n        raise","preventionTips":["Note the replay compares direction exactly ('in'/'out', lowercase)","Write cash events only via add_cash_event","Normalize legacy uppercase/synonym rows during migration"],"tags":["portfolio","validation","cash-ledger","replay","data-corruption"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}