rails 3 active merchant purchasing code - ruby-on-rails

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 support#foo.com 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 support#foo.com 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 support#foo.com"
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 support#foo.com"
return false
end
end
return true
end

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.

Related

Rails Spree Cannot transition from payment to complete

I am integrating a custom pay method into my rails app. But I cannot do that my order goes from payment stat to completed state. I am ensure that the payment registered at the order is at completed state, so I cannot figure out why the state machine are telling me "No payment found"
Here's my code.
def perform_payment payment_id, state
Rails.logger.info("PERFORM PAYMENT => payment_id: #{payment_id}, state: #{state}")
payment = Spree::Payment.find payment_id
return unless payment
order = payment.order
begin
if state == "accepted"
payment.started_processing!
payment.capture!
Rails.logger.info("PERFORM PAYMENT => order_id:#{order.id}, current_order_state: #{order.state}")
Rails.logger.info("PERFORM PAYMENT => order_payments:#{order.payments.size}")
order.next! unless order.state == "completed"
elsif state == "rejected"
payment.started_processing!
payment.failure!
end
rescue Exception => e
Rails.logger.error("Error al procesar pago orden #{order.number}: E -> #{e.message}")
return false
end
end
Here's the error that I'm getting
StateMachines::InvalidTransition (Cannot transition state via :next from :payment (Reason(s): No payment found))
Actually, It was #Alex mentions. It's only neccesary the .netx! on the order. To do that. you need to ensure that you have a payment in "completed" state.

How to refactor this code to make it more readable and efficient?

I need help refactoring the code.I ve tried by best landed with the following code. Is there anything That I can do
class OrdersController < ApplicationController
before_action :get_cart
before_action :set_credit_details, only: [:create]
# process order
def create
#order = Order.new(order_params)
# Add items from cart to order's ordered_items association
#cart.ordered_items.each do |item|
#order.ordered_items << item
end
# Add shipping and tax to order total
#order.total = case params[:order][:shipping_method]
when 'ground'
(#order.taxed_total).round(2)
when 'two-day'
#order.taxed_total + (15.75).round(2)
when "overnight"
#order.taxed_total + (25).round(2)
end
# Process credit card
# Check if card is valid
if #credit_card.valid?
billing_address = {
name: "#{params[:billing_first_name]} # .
{params[:billing_last_name]}",
address1: params[:billing_address_line_1],
city: params[:billing_city], state: params[:billing_state],
country: 'US',zip: params[:billing_zip],
phone: params[:billing_phone]
}
options = { address: {}, billing_address: billing_address }
# Make the purchase through ActiveMerchant
charge_amount = (#order.total.to_f * 100).to_i
response = ActiveMerchant::Billing::AuthorizeNetGateway.new(
login: ENV["AUTHORIZE_LOGIN"],
password: ENV["AUTHORIZE_PASSWORD"]
).purchase(charge_amount, #credit_card, options)
unless response.success?
#order.errors.add(:error, "We couldn't process your credit
card")
end
else
#order.errors.add(:error, "Your credit card seems to be invalid")
flash[:error] = "There was a problem processing your order. Please try again."
render :new && return
end
#order.order_status = 'processed'
if #order.save
# get rid of cart
Cart.destroy(session[:cart_id])
# send order confirmation email
OrderMailer.order_confirmation(order_params[:billing_email], session[:order_id]).deliver
flash[:success] = "You successfully ordered!"
redirect_to confirmation_orders_path
else
flash[:error] = "There was a problem processing your order. Please try again."
render :new
end
end
private
def order_params
params.require(:order).permit!
end
def get_cart
#cart = Cart.find(session[:cart_id])
rescue ActiveRecord::RecordNotFound
end
def set_credit_details
# Get credit card object from ActiveMerchant
#credit_card = ActiveMerchant::Billing::CreditCard.new(
number: params[:card_info][:card_number],
month: params[:card_info][:card_expiration_month],
year: params[:card_info][:card_expiration_year],
verification_value: params[:card_info][:cvv],
first_name: params[:card_info][:card_first_name],
last_name: params[:card_info][:card_last_name],
type: get_card_type # Get the card type
)
end
def get_card_type
length, number = params[:card_info][:card_number].size, params[:card_info][:card_number]
case
when length == 15 && number =~ /^(34|37)/
"AMEX"
when length == 16 && number =~ /^6011/
"Discover"
when length == 16 && number =~ /^5[1-5]/
"MasterCard"
when (length == 13 || length == 16) && number =~ /^4/
"Visa"
else
"Unknown"
end
end
end
Products with a price attribute. We have shopping Carts that have many Products through the OrderedItems join table. An OrderedItem belongs_to a Cart and a Product. It has a quantity attribute to keep track of the number of products ordered.
The OrderedItem also belongs_to an Order
I wanted to know if it can be refactored further.
First of all you should move all that business logic from the controller into models and services (OrderProcessService, PaymentService). All the controller's private methods belong to a PaymentService.
Split the code into smaller methods.
If doing that on the model level some things that come into my mind when reading your code are the following:
#order.add_items_from_cart(#cart)
#order.add_shipping_and_tax(shipping_method)
Orders should be first saved (persisted in DB), then processed (purchased with changing their status).
#order.save might fail after a successful payment, so a client will lose the money and not get their order.
the purchasing is an important and critical process, so you should make sure everything is ready for it (the order is valid and saved)
a client should be able to purchase later or after the payment page is accidentally reloaded without filling the form again
normally when a payment is performed you should send an order ID to the payment system. The payment system will store the ID and you will always know which order the payment belongs to.
There are a lot of other things to consider. You have a lot of work to do.

How to avoid a subscription from going out of sync with Stripe?

Given the following method
def change_plan_to(plan_id)
new_plan = Plan.find plan_id
stripe_customer = Stripe::Customer.retrieve(stripe_customer_token)
stripe_customer.update_subscription(plan: new_plan.slug)
self.plan = new_plan
self.active = true
save
rescue Stripe::InvalidRequestError => e
logger.error "[STRIPE] #{ e }"
errors.add :base, "Unable to change your plan!"
false
end
Specifically line #4-6. I want 4 and 5 to happen only if 4 is successful but Stripe doesn't return the ability to wrap that in a if. If it errors it just throws Stripe::InvalidRequestError.
What's the best way to handle this? Fire & forget and allow Stripe webhook callbacks to manage expiring active state as needed?
The other scenario is that all the code will halt after line 4 if it doesn't pass. Is this how rescue works?
Yes thats the way rescue work,
So better you execute these statements which is dependent on line 4 in webhook callbacks that stripe sends to you. Because that ensures subscription change.

Rails 3: loops and plucking items out best practices

I am working on a small app that allows for users to add a product (or subscription) to their cart. Upon creating their account, the new user is sent to a "bundle" page where it asks if they would like to add a different subscription to a different product altogether for a bundled price.
Here is where I am stuck: Upon submitting the user's credit card info I get slightly "lost in translation" when trying to setup the bundle pricing to submit to Authorize.net (I understand how to authnet, not the question here).
Here is what I have so far:
current_order.products.includes(:client).each do |product|
transaction = current_order.submit_order_to_authnet(product)
if transaction.result_code == 'Ok'
new_group = Group.create!(:name => "#{current_user.full_name} #{product.title}", :type => 'school', :start_date => Time.now, :status => 'active', :site_id => 1)
primary = session[:primary_product_id].eql?(product.id) ? true : false
# Add subscription to Group
new_group.add_subscription(product, current_order, transaction.subscription_id, 'active', primary)
# Add Subscription to CurrentOrder
current_order.subscriptions << new_group.subscriptions.last
# Add user to NewGroup
current_user.groups << new_group
# Create New Group Admin
new_group.group_admins.create(:user_id => current_user.id)
# Send success email
OrderMailer.checkout_confirmation(current_user).deliver
else
errors << transaction.result_code
end
end
I am trying to figure out the best solution when it comes to looping through each product in the users current_order because the second subscription in the users cart is the subscription that gets the discount applied too. I know I can write something like this:
current_order.products.includes(:client).each do |product|
if current_order.products.many? and product == current_order.products.last
# run discount logic
else
# continue with authnet for single subscription
end
end
But I am just not sure if that is a best practice or not. Thoughts?
So the only subscription that doesn't get discounted is the first one? Why not write it like this:
current_order.products.includes(:client).each do |product|
if product == current_order.products.first
# continue with authnet for single subscription
else
# run discount logic
end
end

rails increment on no error

I have a rails code that sends emails. Following is in my controller:
def create
#users = Users.find(:all)
#sub = params[:sub]
#body = params[:body]
#index = 0
#users.each {|i| index++; Notifier.deliver_notification(#users.email_address, #sub, #body, #users.unsubscribe_link);}
flash[:notice] = "Mail was sent to " + #index + " people"
end
I have the following in my Model
class Notifier < ActionMailer::Base
def notification(email, sub, content, link)
recipients email
from "my_email#example.com"
subject sub
body :content => recipient, :link => link
end
end
This all works fine. My Question is:
For example if there is an error in sending mail to one of the pople, even then my flash message will say. Mail was sent to X people
What can I do to ensure that #index gets incremented ONLY when mail is successfully sent?
The deliver_notification method should always return a TMail object regardless of success or failure. There is a raise_delivery_errors setting which will allow the mailer to raise exceptions if there's trouble, but you'll have to rescue these in your block and only increment on success.
Due to the way mail is delivered by ActionMailer, it's often the case you won't know if the message is successful or not. Email is usually queued and delivered at a point in time well beyond your method call, and most errors occur at this point due to any number of difficulties in delivery. It's only wildly malformed email addresses that will be rejected up front, or if the mail delivery mechanism is non-functional.
Edit: Added Exception Tracking
count = 0
#users.each do |user|
begin
Notifier.deliver_notification(
user.email_address,
#sub,
#body,
user.unsubscribe_link
)
count += 1
rescue => e
# Something went wrong, should probably store these and examine them, or
# at the very least use Rails.logger
end
end
flash[:notice] = "Mail was sent to #{count} people"
Your example used index++ which is not supported by Ruby. What you probably want is index += 1. You were also using the #users array directly instead of the individual elements.
You could ask ActionMailer to throw exceptions for you, and then only count those deliveries that don't result in an exception.
ActionMailer::Base.raise_delivery_errors = true
#users.each do |i|
begin
Notifier.deliver_notification(#users.email_address, #sub, #body, #users.unsubscribe_link)
index++
rescue Exception => e
# Do whatever you want with the failed deliveries here
end
end

Resources