{"record":{"id":"066fad4883c00fe3","repo":"we-promise/sure","slug":"statement-file-has-already-been-uploaded","errorCode":null,"errorMessage":"Statement file has already been uploaded","messagePattern":"Statement file has already been uploaded","errorType":"validation","errorClass":"AccountStatement::DuplicateUploadError","httpStatus":null,"severity":"warning","filePath":"app/models/account_statement.rb","lineNumber":87,"sourceCode":"    month_start = month.to_date.beginning_of_month\n    month_end = month_start.end_of_month\n    where(\"period_start_on <= ? AND period_end_on >= ?\", month_end, month_start)\n  }\n\n  class << self\n    def statement_manager?(user)\n      user&.admin? || user&.member?\n    end\n\n    def create_from_upload!(family:, account:, file:)\n      prepared_upload = prepare_upload!(file)\n      create_from_prepared_upload!(family: family, account: account, prepared_upload: prepared_upload)\n    end\n\n    def create_from_prepared_upload!(family:, account:, prepared_upload:)\n      statement = nil\n      duplicate = duplicate_for(family, prepared_upload)\n      raise DuplicateUploadError, duplicate if duplicate\n\n      statement = family.account_statements.build(\n        account: account,\n        filename: prepared_upload.filename,\n        content_type: prepared_upload.content_type,\n        byte_size: prepared_upload.byte_size,\n        checksum: prepared_upload.checksum,\n        content_sha256: prepared_upload.content_sha256,\n        source: :manual_upload,\n        upload_status: :stored,\n        review_status: account.present? ? :linked : :unmatched,\n        currency: account&.currency || family.currency\n      )\n\n      statement.original_file.attach(\n        io: StringIO.new(prepared_upload.content),\n        filename: prepared_upload.filename,\n        content_type: prepared_upload.content_type","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/account_statement.rb#L69-L105","documentation":"AccountStatement.create_from_upload!/create_from_prepared_upload! computes an identity for each upload (MD5 checksum plus SHA-256 of content) and runs duplicate_for against the family's existing statements before building the record. If an already-stored statement has the same content identity, the method raises AccountStatement::DuplicateUploadError (message \"Statement file has already been uploaded\") carrying the duplicate, preventing byte-identical files from entering review twice. This variant (:87) is the pre-check path: the duplicate is detected up front, before any save.","triggerScenarios":"Uploading the same statement PDF twice from the statement upload UI; re-uploading after a network retry where the first attempt actually succeeded but the user didn't notice; uploading the same file to two different accounts within the same family (duplicate check is family-scoped); a file re-exported byte-identically by the bank (same export = same checksum) months later.","commonSituations":"Double-click on the submit button with slow feedback; browser back-button resubmission; syncing statements via automation that doesn't record what was already pushed; banks that produce deterministic PDFs so 'different months' are actually identical files when content didn't change.","solutions":["Don't re-upload — open the statement list and confirm the file is already there (that's what the error is telling you)","If you genuinely need it twice (e.g. joint accounts), note the check is per-family per content: export the file again so it differs (many banks add a generation timestamp), or deduplicate your intent first","Guard clients/automation: check for an existing statement with the same checksum before posting (compute MD5 base64 + SHA-256 hex of the bytes)","Clear the duplicate record first if the original was uploaded by mistake (delete the old statement, then re-upload)"],"exampleFix":"# before\nAccountStatement.create_from_upload!(family: family, account: account, file: file)\n# => AccountStatement::DuplicateUploadError: Statement file has already been uploaded\n\n# after: pre-check the same identity the model uses\nchecksum = Digest::MD5.base64digest(content)\nsha = Digest::SHA256.hexdigest(content)\nexisting = family.account_statements.find_by(checksum: checksum, content_sha256: sha)\nreturn existing if existing\nAccountStatement.create_from_upload!(family: family, account: account, file: file)","handlingStrategy":"validation","validationCode":"# Before uploading\nchecksum = Digest::MD5.base64digest(File.binread(path))\nsha = Digest::SHA256.hexdigest(File.binread(path))\nif family.account_statements.exists?(checksum: checksum, content_sha256: sha)\n  # already uploaded: open it instead of re-uploading\nend","typeGuard":"def duplicate_statement?(family, path)\n  content = File.binread(path)\n  family.account_statements.exists?(\n    checksum: Digest::MD5.base64digest(content),\n    content_sha256: Digest::SHA256.hexdigest(content)\n  )\nend","tryCatchPattern":"rescue AccountStatement::DuplicateUploadError => e\n  # e.message carries the duplicate record; treat as idempotent success, don't retry\n  statement = family.account_statements.find_by(content_sha256: computed_sha)\n  redirect_to statement, notice: \"Already uploaded\"\nend","preventionTips":["Disable the submit button after first click to avoid double uploads","Have automation track uploaded checksums and skip known files","Remember the check is family- and byte-scoped: re-exported (changed) files pass","After an uncertain network error, check the statement list before re-uploading"],"tags":["rails","file-upload","duplicate-detection","statements","checksum"],"backgroundTag":"duplicate-upload","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}