New Discussion

Notifications

You’re not receiving notifications from this thread.

How do I allow a user to make multiple payments on one booking using Stripe?

38
General

I'm building an events app using Rails and Stripe to handle payments. I've used javascript for my booking page in order to allow a user to book and pay for multiple spaces rather than just one at a time. However, when I do a test Stripe payment it's only processing the cost for one space. So if an event costs £10 and I want to book 4 spaces, my booking page allows me to indicate this and shows a cost of £40 but the payment on my Stripe dashboard only shows £10.

How do I rectify this?

I have a bookings controller for my Stripe processing code -

def create
    # actually process the booking
    @event = Event.find(params[:event_id])
    @booking = @event.bookings.new(booking_params)
    @booking.user = current_user


    if @booking.save

        # CHARGE THE USER WHO'S BOOKED
        Stripe::Charge.create(amount: @event.price_pennies, currency: "gbp",
            card: @booking.stripe_token, description: "Booking number #{@booking.id}", items: [{quantity: @booking.quantity}])

        flash[:success] = "Your place on our event has been booked"
        redirect_to event_path(@event)
    else
        flash[:error] = "Payment unsuccessful"
        render "new"
    end

    if @event.is_free?

        @booking.save!
        flash[:success] = "Your place on our event has been booked"
        redirect_to event_path(@event)
    end
end

I'm sure this is all basic MVC stuff but I cannot find the solution. I think it has to do with the amount: @event.price_pennies and/or how to handle the items: quantity array but I'm not sure. I don't think its anything to do with my views code so haven't put this up on here.

This is my booking params code with quantity included. I don't have a column in my bookings schema table for total_amount - should I add this and change @event.price_pennies to @booking.total_amount ?

  private

def booking_params
    params.require(:booking).permit(:stripe_token, :quantity)
end

Hey Mike,

So your code here creates a charge for the event price, but you need to actually multiply that by the number of seats in the booking. You've also got some potential issues here in that the booking might save but the payment fails letting the user get away with a free event.

It's probably good to add the total_amount so you can save that to the model in case the price changes later down the line for some reason.

So what I would do is refactor your code here into a method on the model as a start. This will simplify your controller and clean things up in a way that's a lot more manageable.

I would do something like this:

class Booking < ActiveRecord::Base
  def reserve
    # Don't process this booking if it isn't valid
    return unless valid?

    # Free events don't need to do anything special
    if event.is_free?
      save

    # Paid events should charge the customer's card
    else
      begin
        charge = Stripe::Charge.create(amount: @event.price_pennies, currency: "gbp", card: @booking.stripe_token, description: "Booking number #{@booking.id}", items: [{quantity: @booking.quantity}])
        self.stripe_charge_id = charge.id
        save
      rescue Stripe::CardError => e
        errors.add(:base, e.message)
        false
      end
  end
end
def create
    # actually process the booking
    @event = Event.find(params[:event_id])
    @booking = @event.bookings.new(booking_params)
    @booking.user = current_user

    if @booking.reserve
        flash[:success] = "Your place on our event has been booked"
        redirect_to event_path(@event)
    else
        flash[:error] = "Booking unsuccessful"
        render "new"
    end
end

Now this is mostly just psuedo code, nothing really tested by the gist is this:

  1. If you take all the logic and condense it into a function inside the booking, you can have it handle the free and paid events. You can have it decide what it needs to do to reserve
  2. If it's free, you simply just save the model and return the true or false
  3. If it's paid, you try to charge the card, if that fails, you return the error and false
  4. If the payment is successful you save a reference to the stripe charge and then save the record and return the result

You can still refactor this even further to separate out the free and paid reservations to simplify those a bit as well, but I'll leave that up to you. The main point is that by putting this into the model, you can create a new method called "reserve" that handles the logic for doing a reservation AND potentially payment all in one operation rather than trying to do them separately in your controller where things would get really messy.

This is just a start, and as things get more complicated you'll want to refactor this, probably into a service object of some kind, and retain the logic there, but it's should showcase the way you can begin to organize and group the work you need to do a bit better. Hope this helps!

Hi Chris, I really appreciate such a detailed response. So, what your saying is I need to strip out a lot of the logic I'm using in my controller and placing this in my model? This is something which I've felt I needed to do but wasn't quite sure how to go about it.

If I add total_amount to my bookings schema, should I add a method in my model like -

def total_amount
@booking.quantity * @event.price
end

What I mean is, would this help resolve the specific issue surrounding one user booking more than one space on a specific event or does the logic you've outlined above clear that up?

Yep! That will also give you the benefit of making things more more cohesive from the controller perpsective because it only has to know if it worked or not, rather than knowing how to reserve different types of bookings.

So I actually forgot to include the total_amount bit. I would actually add a column in your database for it and set it at reservation.

class Booking < ActiveRecord::Base
  def reserve
    # Don't process this booking if it isn't valid
    return unless valid?

    # We can always set this, even for free events because their price will be 0.
    self.total_amount = quantity * event.price_pennies

    # Free events don't need to do anything special
    if event.is_free?
      save

    # Paid events should charge the customer's card
    else
      begin
        charge = Stripe::Charge.create(amount: total_amount, currency: "gbp", card: @booking.stripe_token, description: "Booking number #{@booking.id}", items: [{quantity: @booking.quantity}])
        self.stripe_charge_id = charge.id
        save
      rescue Stripe::CardError => e
        errors.add(:base, e.message)
        false
      end
  end
end

So you can set it up to always save the total_amount to store for later (like a receipt would do). Free events it should just be quantity * 0. Make sure that the price for free events returns 0 by default if it doesn't already, and then you can use that total amount to charge with Stripe. Save it in pennies so that you can make sure you don't have rounding errors or anything and just store as integers because they're safer. You'll have to do formatting on the UI to print out in decimal format, but that's totally fine.

So, when I add total_amount as a column in my bookings table I add the datatype as 'integer'? I have price and price_pennies as integers also but I wasn't sure whether this had to be as a float or decimal as there will be prices like £8.99 or £4.50 for example but you say I can change this in the view?

Pretty much! Stripe will require you to send over the price in cents as an integer, so it's usually good to keep that the same in your app. So for your examples, they'd be stored as 899 and 450 in the database as integers. Then you divide by 100.0 to get £8.99.

This way you don't have to go back and forth converting things and to and from cents and you would only do that for display purposes. Feels a little weird at first but dramatically reduces your risk of mistakes with money which is good.

I must be already doing this as they show as that now, although I've been building this thing for so long I've forgotten doing it haha!!

Okay, I've implemented the above but now I get this error -

  NoMethodError in BookingsController#create
  undefined method `*' for nil:NilClass

In relation to the total_amount method in my Booking model -

  self.total_amount = quantity * event.price_pennies

Should it be booking.quantity ?

Yeah, so you'll probably want to make sure that the quantity on a booking is always 1 or more and your event's price_pennies should return 0 if the event is free, and greater than 0 if it is paid. That way you always have these numbers and you don't need to add any more if statements to these other calculations.

I assumed I'd done this but obviously not. Where do I set the defaults for these - in the controller or model?

I would probably add a validation for the quantity on the Booking, and then you may want to add a before_save callback for Event to set to 0 if it is free.

before_save :set_zero_price_pennies, if: :is_free?

def set_zero_price_pennies
  self.price_pennies = 0
end

And you can also validate that Event always has a value for price_pennies of greater than or equal to 0 so that that column is also never nil.

I've just noticed that price_pennies is set at this in my schema -

 t.integer  "price_pennies",      default: 0,     null: false

Does this change anything regarding the above discussion? I think the issue may surround quantity as there's no default value set for that.

Yeah exactly, this would take care of it on a database level which would mean that your could should always have a value. And then not having a default on the booking is probably the thing that triggered the nil error. It doesn't super matter if you have a default value there, but it probably does make sense to add a validation requiring you to always have 1+ quantity on the booking so that value is never nil.

I'm still not solving this. I seem to be bouncing from one issue to another. Can't believe how difficult it is to create a process whereby a user can make multiple bookings in one payment.

Do I need to put something in my seeds file to handle the default value for quantity? I've tried the following -

 Booking.find_or_create_by_name('my_booking', quantity:1)

But I get a 'No method error : undefined method error' on my command line when I do rake db:seed. As I said I'm bouncing all over the place with this so apologies if I keep coming back looking for answers but I'm really stabbing in the dark as I've never done this before and I'm struggling to find solutions on my own.

Gonna have to see your stacktrace regarding that error. I think maybe you're just having trouble wrapping your head around everything. I had trouble with it for a while the first time I was trying to register and purchase at the same time.

As far as making sure your quantity is required:

validates :quantity, presence: true, numericality: { greater_than: 0 }

For the free events (as previously discussed above) should I do something like this -

before_save : set_price_to_zero_if_free

def set_price_to_zero_if_free
       self.event.price >= 1 ( or 0?)    unless self.event.is_free
end

Well, so you don't need that anymore because you said you've already got your database defaulting the column to 0. 👍 Free ones would be defaulted to 0, and your paid ones will always have a value because it'll be required in the form.

Ah okay. I appear to be making progress - there's no error messages now but I'm getting "Booking unsuccessful" everytime I complete a test payment.

Inspect all the steps in the reserve method and print out the calculations and such. There's probably a piece in there that's not correct and needs a little fixing. You can use byebug to play with those calculations interactively which I find to be super helpful for debugging.

Rather than @booking.stripe_token (etc) should it just be self.stripe_token or just stripe_token as its in the model rather than the controller?

I'll check out byebug.

Ah yup! That is probably it. Since you're inside the instance rather than the controller you can just reference it as stripe_token. And the same with all the other references.

I guess one thing also to note is that since the record hasn't been saved, it won't be able to set the Booking ID in the Stripe description until after the record saves. Maybe initiate an update to store the booking ID reference on there after the save is a better plan.

Still getting 'Booking unsuccessful' - I'll try the byebug option and get to the bottom of it. I can't be far off now if it's not throwing up errors. I really appreciate your patience - this has had me stumped for longer than I care to admit...

Glad I can help, even if it's slow goin' for a bit. Do this a few times and you'll get really comfortable debugging these more complicated processes and be doing them often in no time! :)

And believe me...I've probably wasted months or more of my life to debugging all the various things over the years...

I'm looking at byebug to try and sort this out but not totally sure where I should plant it on my code to identify the issue. I could put it here on my Booking model -

    def reserve
    byebug
    # Don't process this booking if it isn't valid
    return unless valid?

Or here -

  # Paid events should charge the customer's card
    else
     byebug
        begin
        charge = Stripe::Charge.create(amount: total_amount, currency: "gbp", card: stripe_token, description: "Booking number #{Booking.id}", items: [{quantity: quantity}])
        self.stripe_charge_id = charge.id
        save

Or in my controller here -

 def create
    byebug
    # actually process the booking
    @event = Event.find(params[:event_id])
    @booking = @event.bookings.new(booking_params)
    @booking.user = current_user

        if @booking.reserve
            flash[:success] = "Your place on our event has been booked"
            redirect_to event_path(@event)
        else
            flash[:error] = "Booking unsuccessful"
            render "new"
        end

I guess I may have to try them all. What's becoming obvious is that the @booking.reserve function isn't working, hence the flash error is being put into action. My gut feeling is that it must be to do with how the Stripe payment has been implemented and feel it may be hinging on this line -

  self.stripe_charge_id = charge.id

Chris, should I be doing something in addition to this line in order for the whole 'reserve' method to work? Do I need a charge.id column in my bookings table? Do I do something in my controller whereby I pass either charge.id or event.id to my create action? Or do I change charge.id with booking.id which is a column already in my bookings table?

(When I finally get this working I'm gonna party like its 1999!!!!)

Yeah so I would start adding byebug into the reserve method to figure out which branch it's going down when it fails, and which things you thought were working but aren't. Then you can figure out what inputs you're giving it that don't work.

Put a byebug into the Stripe charge section only, you'll know if it gets to that, you are in the right branch. Then you can use byebug to run the charge in the console to see if it succeeds or fails.

You'd want a stripe_charge_id column (as string) on your booking. The charge object is just the result of the Stripe charge, not your record. That's just saving the reference for everything.

The other thing to check is are your charges showing up in the Stripe test dashboard?

Many thanks. Should I set stripe_charge_id into my booking params? This is what I have so far -

  private

def booking_params
    params.require(:booking).permit(:stripe_token, :quantity, :event_id)
end

Just to answer the last part - no, the payments aren't showing up in Stripe.

I've had a go at debugging but nothing springing up as yet. My theory is that @booking.reserve is simply not being called at all, hence why the code is jumping straight to the error message on the if/else statement in the controller. Does there need to be a reference of some kind in my views? Here's the booking.new.html.erb code (is it as simple as a naming issue, should this be booking.create rather than booking.new or does the booking.reserve method need to be referenced in the new action in the controller? -

 <div class="col-md-6 col-md-offset-3" id="eventshow">
  <div class="row">
      <div class="panel panel-default">
          <div class="panel-heading">
                 <h2>Confirm Your Booking</h2>
         </div>
              <div class="calculate-total">
                          <p>
                              Confirm number of spaces you wish to book here:
                                <input type="number" placeholder="1"  min="1" value="1" class="num-spaces">
                          </p>
                            <p>
                                Total Amount
                                £<span class="total" data-unit-cost="<%= @event.price %>">0</span>
                            </p>
                      </div>





            <%= simple_form_for [@event, @booking], id: "new_booking" do |form| %>



             <span class="payment-errors"></span>

            <div class="form-row">
                <label>
                  <span>Card Number</span>
                  <input type="text" size="20" data-stripe="number"/>
                </label>
            </div>

            <div class="form-row">
              <label>
              <span>CVC</span>
              <input type="text" size="4" data-stripe="cvc"/>
              </label>
            </div>

            <div class="form-row">
                <label>
                    <span>Expiration (MM/YYYY)</span>
                    <input type="text" size="2" data-stripe="exp-month"/>
                </label>
                <span> / </span>
                <input type="text" size="4" data-stripe="exp-year"/>
            </div>
        </div>
        <div class="panel-footer">    

           <%= form.button :submit %>


             </div> 

   <% end %>
    <% end %>

         </div>
     </div>
   </div>  

    <script type="text/javascript">
        $('.calculate-total input').on('keyup change', calculateBookingPrice);

   function calculateBookingPrice() {
     var unitCost = parseFloat($('.calculate-total .total').data('unit-cost')),
         numSpaces = parseInt($('.calculate-total .num-spaces').val()),
         total = (numSpaces * unitCost).toFixed(2);

     if (isNaN(total)) {
       total = 0;
     }

     $('.calculate-total span.total').text(total);
   }

     $(document).ready(calculateBookingPrice)

   </script>



   <script type="text/javascript" src="https://js.stripe.com/v2/"></script>

  <script type="text/javascript">
     Stripe.setPublishableKey('<%= STRIPE_PUBLIC_KEY %>');
     var stripeResponseHandler = function(status, response) {
       var $form = $('#new_booking');

       if (response.error) {
       // Show the errors on the form
       $form.find('.payment-errors').text(response.error.message);
       $form.find('input[type=submit]').prop('disabled', false);
       } else {
       // token contains id, last4, and card type
       var token = response.id;
       // Insert the token into the form so it gets submitted to the server
       $form.append($('<input type="hidden" name="booking[stripe_token]"     />').val(token));
       // and submit
       $form.get(0).submit();
       }
     };

     // jQuery(function($)  { - changed to the line below
     $(document).on("ready page:load", function () {

       $('#new_booking').submit(function(event) {
        var $form = $(this);

        // Disable the submit button to prevent repeated clicks
         $form.find('input[type=submit]').prop('disabled', true);

         Stripe.card.createToken($form, stripeResponseHandler);

         // Prevent the form from submitting with the default action
         return false;
       });
     });
   </script>

If it's not getting called at all, check that your route is getting called and the action as well. Your Javascript looks like it will submit the form which should go to the create action of the events booking controller.

The thing is that if it jumps straight to your else statement in the action, then it definitely ran the reserve method. Are you sure that your record is valid? The first line of the reserve method doesn't attempt to process the booking unless it is valid which means it terminates and returns instantly if the booking doesn't have a valid set of attributes. That's probably your issue (gotta even check those little tiny things) if it's terminating before it gets to your other code.

Great minds etc - just literally tried commenting out that line and now getting errors again so IT WAS this line -

      return unless valid?

Had to comment out the total_amount method because that wasn't working ( I've never been able to get that to work which is frustrating as hell) and now I need to provide a source or customer for my stripe code block. I assumed it was this -
:source (params[:stripeToken])

But no, so I'll have to figure this out but at least it's moving again. Not sure why its getting blocked at that first line, why would the booking not be valid? Never had an issue with that before. At least it's moving again!!!

Trying printing out the errors on the object after the valid? to see what validations are failing. That might help you figure out what's up with that stuff and get you onwards towards the bigger fish. :)

Still battling with this - one last thought before I go right back to square one and rebuild the whole payment structure - does the API version have any affect here? There's been an update recently, does this need me to re-hash my API keys or anything?

It shouldn't. They lock your account to whatever was the most recent version of the API when you signed up so that you'll have seamless integrations and their docs will show you what's available from the version you're on as long as you're signed in.

You can upgrade but it won't likely affect anything for you. What's the latest? Have you narrowed down what's going on?

It just seems to be bouncing around all over the place. i spotted one error earlier today but now I'm getting 'missing required param :amount'. I'm following your Stripe implementation video (haven't finished it yet) but its for subscriptions not charges so there may be some fundamental differences. There's something key here that I'm missing with Stripe. When you move code like this from a controller (where it worked fine in terms of processing a payment - except for taking multiple reservations) over to model, what are the key take overs I could be missing?

So there aren't really any differences from my videos for individual charges, just that you create a Charge instead of a Subscription object basically.

As for moving code from controller to model, the only thing that changes is the context. On the controller, your variables and things that are available are all the params and controller stuff. When you move it into the model, you're inside the record itself, so you need to pass in the params you're going to use. That's an important difference, but not too significant.

Maybe an idea for figuring this out (and you may have done this already) would be to create a new app, test this stuff out from scratch and then see if you can get a simple version of this working standalone, and then try to take that and apply it to your actual app. I build lots of throwaway apps to prove out ideas like this and it helps when I can't wrap my head around a problem. Start incrementally, only attempt making a charge, then add a quantity to it, and then once you've got that working add it to your app?

Also don't feel like you need to migrate code into the model yet if you're not ready. You can do all this in the controller if it helps wrap your head around it. At the end of the day, a working app is better than anything else.

Thanks so much for your help on this Chris. I'm really sorry its dragged out. I'll figure this out one way or the other and when I do I'll let you know on here what the final code is:)

By the way, I've only had chance to watch one of your screencasts but its great. Really clear and super easy to follow. Looking forward to watching/learning lots more.

Finally (finally!!!!) I've got to the bottom of this. After a bit of re-jigging the key issue was that when I was updated the price with javascript in the view it was purely updating the 'text' rather than the actual server/database. So 'quantity' wasn't being passed through and, hence, only one amount was being collected. This is the key section in the view now -

  <div class="calculate-total">
                          <p>
                              Confirm number of spaces you wish to book here:
                                <input type="number" placeholder="1" name="booking[quantity]"  min="1" value="1" class="num-spaces">
                          </p>
                            <p>
                                Total Amount
                                £<span class="total" data-unit-cost="<%= @event.price %>">0</span>
                            </p>
                      </div>

They key part here is name="booking[quantity]" so the appropriate parameter is named.

Here's my current code in the Booking model -

  class Booking < ActiveRecord::Base

belongs_to :event
belongs_to :user

#validates :quantity, presence: true, numericality: { greater_than: 0 }
validates :total_amount, presence: true, numericality: { greater_than: 0 }
validates :quantity, :total_amount, presence: true


def reserve
    # Don't process this booking if it isn't valid
    self.valid?

    # We can always set this, even for free events because their price will be 0.
    #self.total_amount = booking.quantity * event.price

            # Free events don't need to do anything special
            if event.is_free?
            save!

            # Paid events should charge the customer's card
            else

                begin
                    self.total_amount = event.price_pennies * self.quantity
                    charge = Stripe::Charge.create(
                        amount: total_amount,
                        currency: "gbp",
                        source: stripe_token, 
                        description: "Booking created for amount #{total_amount}")
                    self.stripe_charge_id = charge.id
                    save!
                rescue Stripe::CardError => e
                errors.add(:base, e.message)
                false
            end
        end 
    #end
end
end

Not sure if there's a system for marking a question as correct on here but this now works. Thanks again for all your assistance. This has set me back a long way but finally I can move forward.

I'm a few days behind catching up on the forum. Great to see you got it working! Having the quantity being passed over sure helps. :) Took a while to debug, but I bet it feels good to have figured it out!

Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 89,127+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.