we-promise/sure · warning · AccountStatement::DuplicateUploadError

Statement file has already been uploaded

Error message

Statement file has already been uploaded

What it means

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.

Source

Thrown at app/models/account_statement.rb:87

    month_start = month.to_date.beginning_of_month
    month_end = month_start.end_of_month
    where("period_start_on <= ? AND period_end_on >= ?", month_end, month_start)
  }

  class << self
    def statement_manager?(user)
      user&.admin? || user&.member?
    end

    def create_from_upload!(family:, account:, file:)
      prepared_upload = prepare_upload!(file)
      create_from_prepared_upload!(family: family, account: account, prepared_upload: prepared_upload)
    end

    def create_from_prepared_upload!(family:, account:, prepared_upload:)
      statement = nil
      duplicate = duplicate_for(family, prepared_upload)
      raise DuplicateUploadError, duplicate if duplicate

      statement = family.account_statements.build(
        account: account,
        filename: prepared_upload.filename,
        content_type: prepared_upload.content_type,
        byte_size: prepared_upload.byte_size,
        checksum: prepared_upload.checksum,
        content_sha256: prepared_upload.content_sha256,
        source: :manual_upload,
        upload_status: :stored,
        review_status: account.present? ? :linked : :unmatched,
        currency: account&.currency || family.currency
      )

      statement.original_file.attach(
        io: StringIO.new(prepared_upload.content),
        filename: prepared_upload.filename,
        content_type: prepared_upload.content_type

View on GitHub (pinned to e69894adb9)

Solutions

  1. Don't re-upload — open the statement list and confirm the file is already there (that's what the error is telling you)
  2. 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
  3. Guard clients/automation: check for an existing statement with the same checksum before posting (compute MD5 base64 + SHA-256 hex of the bytes)
  4. Clear the duplicate record first if the original was uploaded by mistake (delete the old statement, then re-upload)

Example fix

# before
AccountStatement.create_from_upload!(family: family, account: account, file: file)
# => AccountStatement::DuplicateUploadError: Statement file has already been uploaded

# after: pre-check the same identity the model uses
checksum = Digest::MD5.base64digest(content)
sha = Digest::SHA256.hexdigest(content)
existing = family.account_statements.find_by(checksum: checksum, content_sha256: sha)
return existing if existing
AccountStatement.create_from_upload!(family: family, account: account, file: file)
Defensive patterns

Strategy: validation

Validate before calling

# Before uploading
checksum = Digest::MD5.base64digest(File.binread(path))
sha = Digest::SHA256.hexdigest(File.binread(path))
if family.account_statements.exists?(checksum: checksum, content_sha256: sha)
  # already uploaded: open it instead of re-uploading
end

Type guard

def duplicate_statement?(family, path)
  content = File.binread(path)
  family.account_statements.exists?(
    checksum: Digest::MD5.base64digest(content),
    content_sha256: Digest::SHA256.hexdigest(content)
  )
end

Try / catch

rescue AccountStatement::DuplicateUploadError => e
  # e.message carries the duplicate record; treat as idempotent success, don't retry
  statement = family.account_statements.find_by(content_sha256: computed_sha)
  redirect_to statement, notice: "Already uploaded"
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/066fad4883c00fe3. Report an issue: GitHub.