{"record":{"id":"a304e9a869b06d13","repo":"antiwork/gumroad","slug":"attach-at-least-one-file-before-completing-this-co","errorCode":null,"errorMessage":"Attach at least one file before completing this commission.","messagePattern":"Attach at least one file before completing this commission\\.","errorType":"validation","errorClass":"ActiveRecord::RecordInvalid","httpStatus":422,"severity":"error","filePath":"app/models/commission.rb","lineNumber":156,"sourceCode":"      minimum = deposit_purchase.link.currency[\"min_price\"]\n      total = minimum if total.positive? && total < minimum\n      total\n    end\n\n    # Refunding the deposit is how the Help Center tells sellers to reject a commission, and\n    # nothing transitions the commission when they do — so the deposit is re-read at charge time.\n    def ensure_deposit_is_chargeable!\n      return if deposit_is_chargeable?\n\n      errors.add(:base, \"This commission's deposit is no longer in a completable state, so it can no longer be completed.\")\n      raise ActiveRecord::RecordInvalid, self\n    end\n\n    def ensure_deliverable_is_attached!\n      return if files.attached?\n\n      errors.add(:base, \"Attach at least one file before completing this commission.\")\n      raise ActiveRecord::RecordInvalid, self\n    end\n\n    def deposit_is_chargeable?\n      return false unless is_in_progress?\n\n      # A fresh read, not the memoized association — a refund can land through another instance\n      # after this commission was loaded. `find` rather than `reload` because the completion\n      # purchase prices variants from the memoized deposit's loaded association objects.\n      deposit = Purchase.find(deposit_purchase_id)\n      # Including test: a seller buying their own commission product gets a `test_successful`\n      # deposit, and completing it is a supported flow that skips charging entirely.\n      return false unless Purchase::ALL_SUCCESS_STATES_INCLUDING_TEST.include?(deposit.purchase_state)\n\n      !deposit.refunded? &&\n        !deposit.stripe_partially_refunded? &&\n        !deposit.chargedback_not_reversed?\n    end\n","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/models/commission.rb#L138-L174","documentation":"Raised as ActiveRecord::RecordInvalid by Commission#ensure_deliverable_is_attached! (app/models/commission.rb:152) when completing a commission with no ActiveStorage attachments under files. It is a precondition guard on the completion flow: the completion charge bills the buyer for deliverables, so at least one file must exist before the charge is attempted. The commission's errors get the :base message 'Attach at least one file before completing this commission.'.","triggerScenarios":"Invoking commission completion (the complete endpoint / complete! path that calls ensure_deliverable_is_attached!) while commission.files.attached? is false — e.g. the seller never uploaded deliverables, or attachments were removed/purged before completing.","commonSituations":"Seller clicks Complete from a stale page before the upload finished signing/attaching; front-end lets the complete action fire with an empty file list; files were attached to a different record (wrong signed ids) so this record still has none.","solutions":["Disable the Complete action until files.attached? is true, and surface an inline 'attach at least one file' hint.","If using direct-upload signed ids, verify the blobs exist and were attached (files.attached?) before enabling completion.","Rescue ActiveRecord::RecordInvalid on the completion call and show commission.errors.full_messages to the seller."],"exampleFix":"# before\ncommission.complete! # RecordInvalid: no files attached\n\n# after\nunless commission.files.attached?\n  return { error: \"Attach at least one file before completing this commission.\" }\nend\ncommission.complete!","handlingStrategy":"validation","validationCode":"return { error: \"Attach at least one file before completing.\" } unless commission.files.attached?","typeGuard":null,"tryCatchPattern":"begin\n  commission.complete!\nrescue ActiveRecord::RecordInvalid\n  render json: { errors: commission.errors.full_messages }, status: :unprocessable_entity\nend","preventionTips":["Disable the Complete action until ActiveStorage reports files.attached?.","With direct uploads, confirm signed blobs finished attaching before enabling completion.","Verify signed ids belong to this commission, not another record."],"tags":["commissions","rails","activerecord","recordinvalid","activestorage","file-upload","validation"],"backgroundTag":"attachment-presence-validation-failed","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}