{"record":{"id":"cf2afa34112eded5","repo":"we-promise/sure","slug":"couldn-t-find-exchange-rate-from-from-currency","errorCode":null,"errorMessage":"Couldn't find exchange rate from #{from_currency} to #{to_currency} on #{date}","messagePattern":"Couldn't find exchange rate from #(.+?) to #(.+?) on #(.+?)","errorType":"exception","errorClass":"Money::ConversionError","httpStatus":null,"severity":"error","filePath":"lib/money.rb","lineNumber":65,"sourceCode":"  # Priority:\n  #   1. Use custom_rate if explicitly provided (not nil)\n  #   2. Look up rate via store.find_or_fetch_rate\n  #   3. Raise ConversionError if no valid rate available\n  def exchange_to(other_currency, date: Date.current, custom_rate: nil)\n    iso_code = currency.iso_code\n    other_iso_code = Money::Currency.new(other_currency).iso_code\n\n    if iso_code == other_iso_code\n      self\n    else\n      # Use custom rate if provided, otherwise look it up\n      if custom_rate.present?\n        exchange_rate = custom_rate.to_d\n      else\n        exchange_rate = store.find_or_fetch_rate(from: iso_code, to: other_iso_code, date: date)&.rate\n      end\n\n      raise ConversionError.new(from_currency: iso_code, to_currency: other_iso_code, date: date) unless exchange_rate && exchange_rate > 0\n\n      Money.new(amount * exchange_rate, other_iso_code)\n    end\n  end\n\n  def as_json\n    { amount: amount, currency: currency.iso_code, formatted: format }.as_json\n  end\n\n  def <=>(other)\n    raise TypeError, \"Money can only be compared with other Money objects except for 0\" unless other.is_a?(Money) || other.eql?(0)\n\n    if other.is_a?(Numeric)\n      amount <=> other\n    else\n      amount_comparison = amount <=> other.amount\n\n      if amount_comparison == 0","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/lib/money.rb#L47-L83","documentation":"Money#exchange_to looks up a rate via store.find_or_fetch_rate (ExchangeRate) for the given date; if no rate is found - or the rate is nil/zero/negative - it raises Money::ConversionError carrying from_currency, to_currency and date. custom_rate, when supplied, bypasses the lookup entirely.","triggerScenarios":"Converting a balance/transaction between currencies for a date with no stored or synthesizable rate: weekend/holiday dates where markets were closed; exotic pairs with no direct or cross rate (e.g. NZD to TRY); dates older than the exchange-rate history in the database; provider rate fetch disabled or failed so nothing was persisted.","commonSituations":"Portfolio net-worth syncing across many currencies where one minor pair lacks data; back-filling historical balances before the first ExchangeRate record; Yahoo/sync job failures leaving gaps in the rates table; custom date ranges in reports hitting sparse dates.","solutions":["Pass custom_rate when you already know the rate (e.g. a rate captured at transaction time)","Backfill the missing pair/date via the ExchangeRateSynthesizer/job so cross rates get derived","Fallback to the nearest earlier date's rate before giving up (many call sites rescue ConversionError and skip/defer)","Check for zero/negative corrupt rows in exchange_rates - a stored 0 also triggers this raise","For weekends/holidays, synthesize the rate from the previous business day"],"exampleFix":"# before\nusd = money.exchange_to(\"USD\")\n\n# after - explicit rate, else nearest-date fallback\nusd = money.exchange_to(\"USD\", custom_rate: captured_rate) if captured_rate\nusd ||= begin\n  money.exchange_to(\"USD\", date: date)\nrescue Money::ConversionError\n  money.exchange_to(\"USD\", date: ExchangeRate.where(from_currency: money.currency.iso_code, to_currency: \"USD\").order(date: :desc).first&.date)\nend","handlingStrategy":"fallback","validationCode":"rate = ExchangeRate.find_rate(from: from, to: to, date: date) rescue nil\nrate ||= ExchangeRateSynthesizer.derive(from: from, to: to, date: date)\nraise Money::ConversionError.new(from_currency: from, to_currency: to, date: date) unless rate&.positive_rate?","typeGuard":"def convertible?(money, to:, date: Date.current)\n  money.currency.iso_code == Money::Currency.new(to).iso_code ||\n    ExchangeRate.rate_exists?(from: money.currency.iso_code, to: to, date: date)\nrescue Money::Currency::UnknownCurrency\n  false\nend","tryCatchPattern":"begin\n  money.exchange_to(\"USD\", date: date)\nrescue Money::ConversionError => e\n  Rails.logger.warn(\"Missing rate #{e.from_currency}->#{e.to_currency} on #{e.date}; using nearest\")\n  nearest = ExchangeRate.latest_before(e.from_currency, e.to_currency, e.date)\n  nearest ? money.exchange_to(\"USD\", custom_rate: nearest.rate) : skip_conversion(money)\nend","preventionTips":["Always pass custom_rate when the business captured one at transaction time","Run the exchange-rate synthesis job before net-worth/portfolio syncs","Backfill weekend/holiday rates from the prior business day","Alert on ConversionError frequency - spikes indicate the rate-fetch provider is failing"],"tags":["money","exchange-rate","currency-conversion","missing-data","fx"],"backgroundTag":"missing-exchange-rate","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}