{"record":{"id":"c2321f9db9b47a9e","repo":"antiwork/gumroad","slug":"you-can-attach-up-to-disputeevidence-max-custom","errorCode":null,"errorMessage":"You can attach up to #{DisputeEvidence::MAX_CUSTOMER_COMMUNICATION_FILES} files.","messagePattern":"You can attach up to #(.+?) files\\.","errorType":"exception","errorClass":"DisputeEvidence::MergeCustomerCommunicationFilesService::MergeError","httpStatus":null,"severity":"error","filePath":"app/services/dispute_evidence/merge_customer_communication_files_service.rb","lineNumber":45,"sourceCode":"  FILE_TOO_LARGE_MESSAGE = \"One of the uploaded files exceeds the maximum size allowed.\"\n  FILES_TOO_LARGE_MESSAGE = \"The combined size of the uploaded files exceeds the maximum allowed, even after compression. Please remove a file or upload smaller versions.\"\n  UNPROCESSABLE_FILE_MESSAGE = \"One of the uploaded files could not be processed. Please check that every PDF opens correctly and is not password-protected.\"\n  UNSUPPORTED_FILE_TYPE_MESSAGE = \"One of the uploaded files is not a JPG, PNG, or PDF.\"\n\n  def self.perform(blobs:, max_size:)\n    new(blobs:, max_size:).perform\n  end\n\n  def initialize(blobs:, max_size:)\n    @blobs = blobs\n    @max_size = max_size\n  end\n\n  # Returns a new application/pdf ActiveStorage::Blob. The input blobs are left alone: the\n  # caller purges them once the submission has actually been persisted.\n  def perform\n    if blobs.size > DisputeEvidence::MAX_CUSTOMER_COMMUNICATION_FILES\n      raise MergeError, \"You can attach up to #{DisputeEvidence::MAX_CUSTOMER_COMMUNICATION_FILES} files.\"\n    end\n    # The merged output is always application/pdf, so the model's content-type validation never\n    # sees the inputs. Everything below hands seller-supplied bytes to ImageMagick and qpdf.\n    unless blobs.all? { _1.content_type.in?(DisputeEvidence::ALLOWED_FILE_CONTENT_TYPES) }\n      raise MergeError, UNSUPPORTED_FILE_TYPE_MESSAGE\n    end\n    raise FilesTooLargeError, FILE_TOO_LARGE_MESSAGE if blobs.any? { _1.byte_size > max_size }\n\n    downloaded_files = download_blobs\n    merged_path = merge_within_size_budget(downloaded_files)\n\n    File.open(merged_path) do |file|\n      ActiveStorage::Blob.create_and_upload!(io: file, filename: MERGED_FILENAME, content_type: \"application/pdf\")\n    end\n  ensure\n    downloaded_files&.each { _1[:tempfile].close! }\n    File.unlink(merged_path) if merged_path && File.exist?(merged_path)\n  end","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/services/dispute_evidence/merge_customer_communication_files_service.rb#L27-L63","documentation":"DisputeEvidence::MergeCustomerCommunicationFilesService#perform raises MergeError when more than DisputeEvidence::MAX_CUSTOMER_COMMUNICATION_FILES (10) blobs are passed. Stripe accepts exactly one file for the customer_communication evidence field, so multiple uploads are merged into a single PDF; this cap bounds the merge input set.","triggerScenarios":"Submitting the dispute evidence form with more than 10 files. Note the controller folds the already-attached customer_communication_file into the merge inputs, so 10 fresh uploads plus 1 previously saved attachment counts as 11 and fails.","commonSituations":"Sellers uploading a long chat history as many separate screenshots; returning to the dispute page across sessions and adding a few more files each time until the cumulative count crosses 10.","solutions":["Keep the total count (including any previously saved attachment) at or below 10","Combine several screenshots into one taller image or one PDF before uploading","Replace the existing attachment with a single consolidated file instead of adding more"],"exampleFix":"# before\n# 9 previously attached + 3 new uploads = 12 blobs\nMergeCustomerCommunicationFilesService.perform(blobs: twelve_blobs, max_size: budget)\n# => MergeError: You can attach up to 10 files.\n\n# after\n# merge screenshots offline into 1 PDF, then attach 1 new upload (total 10)\nMergeCustomerCommunicationFilesService.perform(blobs: ten_blobs, max_size: budget)","handlingStrategy":"validation","validationCode":"if blobs.size > DisputeEvidence::MAX_CUSTOMER_COMMUNICATION_FILES\n  raise \"attach at most #{DisputeEvidence::MAX_CUSTOMER_COMMUNICATION_FILES} files\"\nend","typeGuard":"def within_customer_communication_file_limit?(blobs)\n  blobs.size <= DisputeEvidence::MAX_CUSTOMER_COMMUNICATION_FILES\nend","tryCatchPattern":"begin\n  DisputeEvidence::MergeCustomerCommunicationFilesService.perform(blobs:, max_size: budget)\nrescue DisputeEvidence::MergeCustomerCommunicationFilesService::MergeError => e\n  flash.now[:alert] = e.message # controller already does exactly this\nend","preventionTips":["Count the previously saved attachment too — the controller folds it into the merge inputs","Cap the file picker in the UI at DisputeEvidence::MAX_CUSTOMER_COMMUNICATION_FILES minus any existing attachment","Encourage consolidating many screenshots into one PDF up front"],"tags":["ruby","rails","stripe","dispute-evidence","file-count-limit"],"backgroundTag":"file-count-limit-exceeded","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}