{"record":{"id":"d07d05e73d8a340f","repo":"we-promise/sure","slug":"duplicate-headers","errorCode":"duplicate_headers","errorMessage":"CSV headers normalize to duplicate columns: %{columns}","messagePattern":"CSV headers normalize to duplicate columns: %(.+?)","errorType":"validation","errorClass":"ActiveRecord::RecordInvalid","httpStatus":null,"severity":"error","filePath":"app/models/import.rb","lineNumber":602,"sourceCode":"    def normalized_csv_headers\n      @normalized_csv_headers ||= begin\n        grouped_headers = Array(csv_headers)\n          .filter_map do |header|\n            normalized = normalize_header(header)\n            next if normalized.blank?\n\n            [ normalized, header ]\n          end\n          .group_by(&:first)\n\n        duplicate_headers = grouped_headers.values.filter_map do |headers|\n          originals = headers.map(&:last).uniq\n          originals if originals.many?\n        end\n\n        if duplicate_headers.any?\n          errors.add(:base, :duplicate_headers, columns: duplicate_headers.map { |headers| headers.join(\", \") }.join(\"; \"))\n          raise ActiveRecord::RecordInvalid, self\n        end\n\n        grouped_headers.transform_values { |headers| headers.first.last }\n      end\n    end\n\n    def normalize_header(header)\n      header.to_s.strip.downcase.gsub(/\\*/, \"\").gsub(/[\\s-]+/, \"_\")\n    end\n\n    def parsed_csv\n      return @parsed_csv if defined?(@parsed_csv)\n\n      csv_content = raw_file_str || \"\"\n      if rows_to_skip.to_i > 0\n        csv_content = csv_content.lines.drop(rows_to_skip).join\n      end\n","sourceCodeStart":584,"sourceCodeEnd":620,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/import.rb#L584-L620","documentation":"During CSV header normalization, Import strips and downcases headers, removes '*', and collapses spaces/dashes to underscores (normalize_header). If distinct original headers collapse to the same normalized name ('Date' vs 'date*', 'Transaction Amount' vs 'transaction-amount'), the validation adds :duplicate_headers to :base with the offending column lists and raises ActiveRecord::RecordInvalid, because column mapping must stay unambiguous.","triggerScenarios":"Uploading a CSV whose headers differ only by case, whitespace, asterisks, or dash/underscore separators — normalization maps both originals to one column key, and grouped_headers detects a normalized name with multiple originals.","commonSituations":"Spreadsheet exports that decorate duplicate headers ('Amount', 'AMOUNT', 'Amount*'); concatenated or hand-edited CSVs; exports whose second same-named column is a notes/total column.","solutions":["Open the CSV and rename the headers listed in the error's columns text so each is unique after normalization","Remove decorative '*' suffixes and case variants before upload","If two columns are legitimately identically named, delete or rename one — the importer cannot guess which column maps where"],"exampleFix":"// before\nimport.publish_later # upload already failed validation: duplicate_headers\n\n// after — check before upload\nheaders = CSV.open(file.path, headers: true).read.headers.to_a.compact\nnormalized = headers.map { |h| h.to_s.strip.downcase.gsub(/\\*/, '').gsub(/[\\s-]+/, '_') }\nraise 'CSV headers normalize to duplicates' unless normalized.uniq.length == normalized.length","handlingStrategy":"validation","validationCode":"headers = CSV.open(file.path, headers: true).read.headers.to_a.compact\nnormalized = headers.map { |h| h.to_s.strip.downcase.gsub(/\\*/, '').gsub(/[\\s-]+/, '_') }\nnormalized.uniq.length == normalized.length # false => the model will raise ActiveRecord::RecordInvalid with :duplicate_headers","typeGuard":null,"tryCatchPattern":"rescue ActiveRecord::RecordInvalid => e near the upload flow and read record.errors.details[:base] for the :duplicate_headers code with the offending column lists; show the lists verbatim so the user knows which headers to rename","preventionTips":["Rename or drop case/asterisk/dash variants of the same header before upload","Apply the same normalization check client-side during CSV preview","Treat duplicate normalized headers as unresolvable ambiguity — never guess a mapping"],"tags":["csv","import","headers","validation","duplicates"],"backgroundTag":"duplicate-column-names","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}