{"record":{"id":"be89bbc6a3ff4fb7","repo":"we-promise/sure","slug":"coinstats-did-not-return-a-portfolioid","errorCode":null,"errorMessage":"CoinStats did not return a portfolioId","messagePattern":"CoinStats did not return a portfolioId","errorType":"exception","errorClass":"Provider::Coinstats::Error","httpStatus":null,"severity":"error","filePath":"app/models/coinstats_item/exchange_linker.rb","lineNumber":33,"sourceCode":"  def link\n    return Result.new(success?: false, created_count: 0, errors: [ \"Exchange is required\" ]) if connection_id.blank?\n    return Result.new(success?: false, created_count: 0, errors: [ \"Exchange credentials are required\" ]) if connection_fields.blank?\n\n    created_count = 0\n    exchange = fetch_exchange_definition\n    validate_required_fields!(exchange)\n\n    response = provider.connect_portfolio_exchange(\n      connection_id: connection_id,\n      connection_fields: connection_fields,\n      name: name.presence || default_portfolio_name(exchange)\n    )\n\n    return Result.new(success?: false, created_count: 0, errors: [ response.error.message ]) unless response.success?\n\n    payload = response.data.with_indifferent_access\n    portfolio_id = payload[:portfolioId]\n    raise Provider::Coinstats::Error, \"CoinStats did not return a portfolioId\" if portfolio_id.blank?\n\n    coins = provider.list_portfolio_coins(portfolio_id: portfolio_id)\n\n    ActiveRecord::Base.transaction do\n      coinstats_item.update!(\n        exchange_connection_id: connection_id,\n        exchange_portfolio_id: portfolio_id,\n        institution_id: connection_id,\n        institution_name: exchange[:name],\n        raw_institution_payload: exchange\n      )\n\n      if coins.nil?\n        Rails.logger.warn \"CoinstatsItem::ExchangeLinker - Initial portfolio coin fetch missing for item #{coinstats_item.id} portfolio #{portfolio_id}; deferring local account creation to background sync\"\n      else\n        coinstats_account = exchange_portfolio_account_manager.upsert_account!(\n          coins_data: coins,\n          portfolio_id: portfolio_id,","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/coinstats_item/exchange_linker.rb#L15-L51","documentation":"After provider.connect_portfolio_exchange succeeds, Brex-style ExchangeLinker expects a portfolioId in the response payload. If payload[:portfolioId] is blank it raises Provider::Coinstats::Error: the CoinStats API call worked but the response body did not contain the id needed to persist the exchange link (exchange_portfolio_id, institution fields).","triggerScenarios":"connect_portfolio_exchange(connection_id:, connection_fields:, name:) returns success but the JSON body lacks or empties portfolioId — a CoinStats API schema/version change, a renamed field, or a stubbed/mocked response in tests that omits it.","commonSituations":"CoinStats deploys a breaking response change; VCR cassettes or fixtures stubbing the connect endpoint with an incomplete payload; connecting an exchange type whose response legitimately omits portfolioId.","solutions":["Reproduce connect_portfolio_exchange in console and inspect the raw payload to see the actual response shape","Check CoinStats API changelog/version differences and update the Provider::Coinstats response mapping if the field was renamed","Fix incomplete test stubs/cassettes to include portfolioId","Capture a DebugLogEntry with the payload so support can triage recurring exchanges"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"rescue Provider::Coinstats::Error around the exchange-link call; report a link-failed result with the message and keep the item unlinked (the surrounding ActiveRecord transaction already rolls back the partial link)","preventionTips":["Pin VCR cassettes/stubs for connect_portfolio_exchange to real payloads that include portfolioId","Wrap provider response parsing so missing expected fields surface as a Result failure with payload diagnostics rather than deep exceptions","Log the raw payload (DebugLogEntry) when the field is missing to catch CoinStats schema changes early"],"tags":["coinstats","provider","api-response","exchange-connection"],"backgroundTag":"unexpected-api-response","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}