Stripe: error messages when Creating a Customer &/or Charge - Ruby on Rails

ThinQtv picture ThinQtv · Aug 25, 2012 · Viewed 9.9k times · Source

I am implementing Stripe in my Rails app & get an Invalid Token error when I load the entry form - I haven't submitted customer data yet. I mostly followed the http://railscasts.com/episodes/288-billing-with-stripe tutorial. I made a few modifications because it was somewhat incomplete.

books/show.html.erb is the page where I link to the form:

<b>Title:</b>  <%= @book.title %> </p>
<b>Author:</b>  <% authorid = @book.author %></p>

<%= @book.id %>
<%= link_to "Buy Now", new_purchase_path(:book_id => @book.id) %>

purchases/new.html.erb is where the user fills out info. When this loads, I get the Invalid Token error:

<%= form_for @purchase do |f| %>
  <% if @purchase.errors.any? %>
    <%= pluralize(@purchase.errors.count, "error") %> prohibited this purchase from being saved.
    <% @purchase.errors.full_messages.each do |msg| %>
      <%= msg %>
    <% end %>
  <% end %>

  <%= f.hidden_field :stripe_card_token %>

  <% if @purchase.stripe_card_token.present? %>
    Credit card has been provided.
  <% else %>
    <%= label_tag :card_number, "Credit Card Number" %>
    <%= text_field_tag :card_number, nil, name: nil %><p>

    <%= label_tag :card_code, "Security Code on Card (CVV)" %>
    <%= text_field_tag :card_code, nil, name: nil %><p>

    <%= label_tag :card_month, "Card Expiration" %>
    <%= select_month nil, {add_month_numbers: true}, {name: nil, id: "card_month"} %>
    <%= select_year nil, {start_year: Date.today.year, end_year: Date.today.year+15}, {name: nil, id: "card_year"} %>
  <% end %>

<div id="stripe_error">
  <noscript>JavaScript is not enabled and is required for this form. First enable it in your web browser settings.</noscript>
</div>

  <%= f.submit "Purchase" %>
<% end %>

purchases.js.coffee is pretty much the same as in the tutorial. I added a few alerts. The status according to my Stripe dashboard is 402. It's a POST /v1/tokens error and the Response Body is:

error:
  type: "card_error"
  message: "This card number looks invalid"
  param: "number"

purchases.js.coffee:

jQuery ->
  Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'))
  purchase.setupForm()

purchase =
   setupForm: ->
    $('#new_purchase').submit ->
      $('input[type=submit]').attr('disabled', true)
     if $('#card_number').length
       purchase.processCard()
       false
     else
       true

  processCard: ->
    card =
      number: $('#card_number').val()
      cvc: $('#card_code').val()
      expMonth: $('#card_month').val()
      expYear: $('#card_year').val()
    Stripe.createToken(card, purchase.handleStripeResponse)

  handleStripeResponse: (status, response) ->
    if status == 200
      alert('This token can still be charged.')
      alert(response.id)
      $('#purchase_stripe_card_token').val(response.id)
      $('#new_purchase')[0].submit()
    else
      alert(response.error.message) 
      alert('The token was invalid, or has been used.')
      $('#stripe_error').text(response.error.message)
      $('input[type=submit]').attr('disabled', false)

I've tried a few versions of my purchase.rb model, such as commenting out the Stripe::Charge function, but still get the 402 Token error. However creating the Customer is successful (code 200).

class Purchase < ActiveRecord::Base

  attr_accessible :stripe_customer_token, :author_id, :book_id
  attr_accessor :stripe_card_token

  belongs_to :book

def save_with_payment
  if valid?

    customer = Stripe::Customer.create(
      :description => "customer email", 
      :card => stripe_card_token
    )
    self.stripe_customer_token = customer.id

#    charge = Stripe::Charge.create(  - this code doesn't work either
#      :amount => 1000,
#      :currency => "usd",
#      :card => stripe_card_token,
#      :description => "book title"
#    )
    save!
  end

  rescue Stripe::InvalidRequestError => e
    logger.error "Stripe error while creating customer: #{e.message}"
    errors.add :base, "There was a problem with your credit card."
    false
  end
end

The error I get if I uncomment the Stripe::Charge code is: Stripe::CardError in PurchasesController#create Cannot charge a customer that has no active card

And, the create method in my purchases_controller.rb

def create
  @purchase = Purchase.new(params[:purchase])
  if @purchase.save_with_payment
    redirect_to @purchase, :notice => "Thank you for purchasing this book!"
  else
    render :new
  end
end

Here's my new method in the purchases_controller.rb:

def new

 book = Book.find(params[:book_id])    

 @purchase = book.purchases.build  

end

BUT if I hit the Back button after Submitting the purchase (to go back to the purchase/new.html.erb page), a SECOND 'purchase' is entered into my database and the code for that POST Token in my Stripe log is 200 (pass)!!!

Here's the javascript compiled from coffeescript:

(function() {

var purchase;

jQuery(function() {

Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'));

return purchase.setupForm();

});

purchase = {

setupForm: function() {

 $('#new_purchase').submit(function() {

   return $('input[type=submit]').attr('disabled', true);

 });

 if ($('#card_number').length) {

   purchase.processCard();

   return false;

 } else {

   return true;

 }

},

processCard: function() {

 var card;

 card = {

   number: $('#card_number').val(),

   cvc: $('#card_code').val(),

   expMonth: $('#card_month').val(),

   expYear: $('#card_year').val()

 };

 return Stripe.createToken(card, purchase.handleStripeResponse);

},

handleStripeResponse: function(status, response) {

 if (status === 200) {

   alert('This token can still be charged.');

   alert(response.id);

   $('#purchase_stripe_card_token').val(response.id);

   return $('#new_purchase')[0].submit();

 } else {

   alert(response.error.message);

   alert('The token was invalid, or has been used.');

   $('#stripe_error').text(response.error.message);

   return $('input[type=submit]').attr('disabled', false);

 }

}

};

}).call(this);

Answer

dwhalen picture dwhalen · Aug 28, 2012

As mentioned in my comment above, the indentation of your CoffeeScript is slightly off (whitespace is significant in CS). It's not a syntax error so it still compiles, but the generated JS is not what you're intending. Here is the code properly indented.

purchase =
  setupForm: ->
    $('#new_purchase').submit ->
      $('input[type=submit]').attr('disabled', true)
      if $('#card_number').length
        purchase.processCard()
        false
      else
        true
  # ...

In the JS you posted, the conditional if ($('#card_number').length) exists outside of the anonymous function bound to the submit event. That causes the conditional to run as soon as the page loads, rather than when the form is submitted. Since there is nothing in the '#card_number' input when the page is initially loaded, the conditional's alternative (else branch) is silently executed. Here is what the JS should look like:

$('#new_purchase').submit(function() {
  $('input[type=submit]').attr('disabled', true);
  if ($('#card_number').length) {
    purchase.processCard();
    return false;
  } else {
    return true;
  }
});

The behavior you see when navigating "back" to the new page is the result of the page loading with input in the '#card_number' field (executing the processCard function), which is what you want, just not when you want it.

If my speculations are correct, fixing the indentation in your CoffeeScript is the solution.