Search code examples
ruby-on-railspayment-gatewayactivemerchant

rails 3 active merchant purchasing code


I am new to rails and new to active merchant, just want to know if the following code is good enough for payment processing using active merchant.

As you can see, I am using authorize and capture instead of the purchase method. My main concern is the "brought_quantity" subtraction in the code (and it's counter part, when the payment processing fails), I am not quite sure how to deal with it in case of race condition or error from the payment gateway.

Please note that the variable transactions is a instance variable for a model/table where I store the information of the payment gateways responses.

def purchase(item)
  price = price_in_cents(item.value)
  if !item.can_purchase
    errors[:base] << "We are sorry, all items are sold out at the moment."
    return false
  else
    response = GATEWAY.authorize(price, credit_card, purchase_options)
    transactions.create!(:action => "authorize", :value => price, :params => response)
    #p response
    if response.success?
      item.brought_quantity = item.brought_quantity + 1
      if item.save!
        response = GATEWAY.capture(price, response.authorization)
        transactions.create!(:action => "capture", :value => price, :params => response)
        if !response.success?
          errors[:base] << "There were some problem processing your payment, please either try again or contact us at [email protected] with this error id: 111"
          @rd = RunningDeal.find_by_id(@item.id)
          @rd.brought_quantity = @rd.brought_quantity - 1
          @rd.save!
          return false
        end
      else
        errors[:base] << "We are sorry, all items are sold out at the moment."
        return false
      end
    else
      # problem process their payment, put out error
      errors[:base] << "There were some problem processing your payment, please either try again or contact us at [email protected] with this error id: 111"
      return false
    end
  end
  return true
end

Edit Ok, did some refactoring and here is the updated code, any comments and suggestions are welcome. I removed the ! on transaction.create since that not a important enough operation to raise an exception.

Here is the updated code based on the feedback.

#from running_deal.rb
def decrement_deal_quantity
  self.brought_quantity = self.brought_quantity + 1
  return self.save!
end


def purchase(running_deal)
  price = price_in_cents(running_deal.value)
  if !running_deal.can_purchase
    errors[:base] << "We are sorry, all items are sold out at the moment."
    return false
  else
    auth_resp = GATEWAY.authorize(price, credit_card, purchase_options)
    transactions.create(:action => "authorize", :value => price, :success => auth_resp.success?, :message => auth_resp.message, :authorization => auth_resp.authorization, :params => auth_resp)
    if auth_resp.success?
      begin
        running_deal.decrement_deal_quantity
        cap_resp = GATEWAY.capture(price, auth_resp.authorization)
        transactions.create(:action => "capture", :value => price, :success => cap_resp.success?, :message => cap_resp.message, :authorization => cap_resp.authorization, :params => cap_resp)
      rescue
        GATEWAY.void(auth_resp.authorization, purchase_options) if auth_resp.success?
        errors[:base] << "There were some problem processing your payment, please either try again or contact us at [email protected]"
        return false
      end
    else
      # problem process their payment, put out error
      errors[:base] << "There were some problem processing your payment, please either try again or contact us at [email protected]"
      return false
    end
  end
  return true

end


Solution

  • processing transactions is tricky.

    Couple of thoughts:

    • if authorize succeeds, capture will succeed in 99.9% of cases. You don't really need to worry about this case that much.
    • if something fails in your code after authorize() was successful (like an exception writing to the database) you want to call void() to the gateway, to remove the authorization. Otherwise funds are frozen for 7 days.
    • this code needs to be moved into a model method:

        @rd = RunningDeal.find_by_id(@item.id)
        @rd.brought_quantity = @rd.brought_quantity - 1
        @rd.save!
      
    • you need to add clause to the bottom of your method to catch exceptions, since you are calling create!() not create() (which return true if it saves)

      rescue Exception => e # error handing end

    • it is unclear why if item.save! fails your error message indicates that the item is sold out? That is totally obscure.

    Overall, you want to do someting like this:

    • check if there is enough inventory
    • perform AUTH
    • start db transaction
    • save/update all db objects
    • commit transaction
    • perform CAPTURE
    • catch exception, and if AUTH was successful - perform VOID

    Hope this helps.