How do I allow a user to make multiple payments on one booking using Stripe?
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
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!