{"record":{"id":"129f246f120a70d0","repo":"antiwork/gumroad","slug":"this-commission-s-deposit-is-no-longer-in-a-comple","errorCode":null,"errorMessage":"This commission's deposit is no longer in a completable state, so it can no longer be completed.","messagePattern":"This commission's deposit is no longer in a completable state, so it can no longer be completed\\.","errorType":"validation","errorClass":"ActiveRecord::RecordInvalid","httpStatus":422,"severity":"error","filePath":"app/models/commission.rb","lineNumber":149,"sourceCode":"\n    def once_per_cart_discounted_display_price_cents\n      discount = deposit_purchase.purchase_offer_code_discount\n      return unless discount&.once_per_cart? && !discount.offer_code_is_percent\n      return if discount.pre_discount_displayed_price_cents.blank?\n\n      total = [discount.pre_discount_displayed_price_cents - discount.offer_code_amount, 0].max\n      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.","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/models/commission.rb#L131-L167","documentation":"Raised as ActiveRecord::RecordInvalid by Commission#ensure_deposit_is_chargeable! (app/models/commission.rb:145) during the completion flow when deposit_is_chargeable? returns false. The check is deliberately strict: the commission must be is_in_progress?, the deposit purchase is re-read from the database (Purchase.find, not the memoized association) and must be in a successful state including test_successful, and it must not be refunded, stripe_partially_refunded, or chargedback_not_reversed. The fresh read exists because refunding the deposit is the documented way for sellers to reject a commission, and nothing transitions the commission when that refund lands through another instance.","triggerScenarios":"Calling the commission-complete endpoint when: the commission already left in_progress; the deposit Purchase.purchase_state is not in Purchase::ALL_SUCCESS_STATES_INCLUDING_TEST (failed/errored); or the deposit was refunded, partially refunded on Stripe, or charged back without reversal. Also hit when a refund lands between page load and clicking Complete — the guard re-reads the deposit at charge time and catches it.","commonSituations":"Seller refunds the deposit to reject the commission, then (or a colleague in another session) tries to complete it anyway. Stale UI still showing a Complete button after the buyer charged back the deposit. Completion racing an in-flight refund.","solutions":["Before completing, re-check the same conditions the guard does: commission.is_in_progress? and the freshly-read deposit purchase's state (successful-including-test, not refunded/partially-refunded/charged-back).","If the deposit was refunded intentionally, treat the commission as rejected — do not attempt completion; no state transition exists for refunds by design.","Refresh the commission state in the UI before showing the Complete action so stale sessions don't attempt completion against a changed deposit.","Handle ActiveRecord::RecordInvalid at the completion endpoint and return the model's errors.full_messages as a 422."],"exampleFix":"# before\ncommission.complete! # RecordInvalid: deposit no longer completable\n\n# after\ndef complete_if_chargeable!(commission)\n  deposit = Purchase.find(commission.deposit_purchase_id)\n  chargeable = commission.is_in_progress? &&\n    Purchase::ALL_SUCCESS_STATES_INCLUDING_TEST.include?(deposit.purchase_state) &&\n    !deposit.refunded? && !deposit.stripe_partially_refunded? && !deposit.chargedback_not_reversed?\n  return { rejected: true } unless chargeable\n\n  commission.complete!\nend","handlingStrategy":"validation","validationCode":"deposit = Purchase.find(commission.deposit_purchase_id)\nchargeable = commission.is_in_progress? &&\n  Purchase::ALL_SUCCESS_STATES_INCLUDING_TEST.include?(deposit.purchase_state) &&\n  !deposit.refunded? && !deposit.stripe_partially_refunded? && !deposit.chargedback_not_reversed?\nreturn reject_path unless chargeable","typeGuard":null,"tryCatchPattern":"begin\n  commission.complete!\nrescue ActiveRecord::RecordInvalid\n  render json: { errors: commission.errors.full_messages }, status: :unprocessable_entity\nend","preventionTips":["Treat a refunded deposit as commission rejection — no completion attempt.","Re-check deposit state at click time, not page-load time; refunds land through other instances.","Remember test_successful deposits are completable; only non-success/refunded/charged-back states are not."],"tags":["commissions","rails","activerecord","recordinvalid","payments","refunds","race-condition"],"backgroundTag":"payment-state-guard-rejected","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}