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_typeView on GitHub (pinned to e69894adb9)
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)
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
- 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
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
- AccountStatement::InvalidUploadError
- file_too_large
- invalid_file_type
- record_not_found
- validation_failed
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/066fad4883c00fe3.
Report an issue: GitHub.