I'm building an Events app using Ruby on Rails. I need to create a system for bookings to ensure that an Event doesn't become over-booked. Each event has a finite number of spaces available - how do I ensure that if, for example, 100 spaces are available, 105 bookings are not taken.
These are my thoughts so far, along with some code I've tried but hasn't really worked.
bookings_controller
def create
#event = Event.find(params[:event_id)
if #event.bookings.count >= #event.total_spaces
flash[:warning] = "Sorry, this event is fully booked."
redirect_to root_path
else
#code to save the booking
end
end
In the views -
<% if #event.bookings.count > #event.total_spaces %>
# flash: "This event is fully booked"
<% else %>
# code to make the booking
I'm not sure this is sufficient to achieve my goal. Do I need a more robust method in my Booking model and some validations to cover this?
I've tried a transaction code block -
Booking.transaction do
#event.reload
if #event.bookings.count > #event.number_of_spaces
flash[:warning] = "Sorry, this event is fully booked."
raise ActiveRecord::Rollback, "event is fully booked"
end
end
but that didn't work as it still allowed a user to process a payment BEFORE the flash message showed up & AFTER the transaction had been completed.
I've never built anything like this before so am a little stumped. Any guidance, appreciated.
UPDATE -
Booking.rb
def set_booking
return {result: false, flash: :warning, msg: 'Sorry, this event is fully booked'} if event.bookings.count >= event.total_spaces
if self.event.is_free?
self.total_amount = 0
save!
else
self.total_amount = event.price_pennies * self.quantity
begin
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
# if this fails stripe_charge_id will be null, but in case of update we just set it to nil again
self.stripe_charge_id = nil
# we check in validatition if nil
end
end
{result: true, flash: :success, msg: 'Booking successful!'}
end
bookings_conroller.rb
def create
# actually process the booking
#event = Event.find(params[:event_id])
# as above, the association between events and bookings means -
#booking = #event.bookings.new(booking_params)
#booking.user = current_user
handler = BookingHandler.new(#event)
booking = handler.set_booking(booking_params)
flash[booking[:flash]] = booking[:msg]
redirect_to root_path
# rest of controller code for booking
First of all, it's better to move validation to the model:
class Event < ActiveRecord::Base
validate :validate_availability
private
def validate_availability
errors.add(:base, 'event is fully booked') if bookings.count >= total_spaces
end
end
Also I advise to read about Service Object pattern and use it in controller.
https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services
My first thought here is, remove the booking logic from the controller. The controller should only be concerned with responding to requests with data handed to it- so the bookings.count >= events.total_spaces should be moved into some sort of handler class- like BookingsHandler?
psuedo code--
This handler could take an an event as a single argument,
handler = BookingHandler.new(#event)
With a method inside that does the logic for you:
def book_event(booking_details)
return {result: false, flash: :warning, msg: 'Sorry, this event is fully booked'} if event.bookings.count >= event.total_spaces
. . . # booking code
{result: true, flash: :success, msg: 'Booking successful!'}
end
With a simpler controller
handler = BookingHandler.new(#event)
booking = handler.book_event(params[:booking_details])
flash[booking[:flash]] = booking[:msg]
redirect_to root_path
As for the transaction block- this wouldn't really have any bearing on your situation as it's used to enforce referential integrity during related atmoic actions. So for example, only changing record A if record B is also successfully altered, rolling back any changes within the transaction if either fail.
Hope this helps.
Related
I'm using Stripe for payments on my Rails app and I've hit the error above. I've recently moved a big chunk of my code from my controller to model and this is the first time I've hit this error (I've tested payments before and it never came up). Not really sure why this is coming up now.
Here's my Model code -
Booking.rb
class Booking < ActiveRecord::Base
belongs_to :event
belongs_to :user
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.to_i * event.price_pennies.to_i
# 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
end
And in my controller -
bookings_controller.rb
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
Here's the error message -
I'm pretty new to Rails so apologies if this seems straightforward, any help would be appreciated.
#booking is an instance variable that is only available in the context of the controller/view. Since reserve is an instance method on the model, you probably just want to refer to self or nothing, i.e #booking.method => self.method or method.
I have a fairly straightforward if else statement in a controller as follows:
if citation_array.blank?
flash.now[:error] = "There was a problem saving the publications selected!"
#user = current_user
render 'pubmed_search'
else
citation_array.each do |user_publication|
begin
publication = Publication.new
render_publication(user_publication)
publication.citation = user_publication
publication.user_id = current_user.id
publication.title = #title
publication.authors = #authors
publication.journal = #journal
publication.year = #year
publication.volume = #volume
publication.pages = #pages
if publication.save
next
end
rescue
next
end
end
#user = current_user
redirect_to current_user
return false
end
It is served an array of id's in citation_array and if there are values present it loops throught them saving each publication found by the id's in the array. The render_publication method instantiates the instance variables so don't be concerned with that.
My issue is this. Very rarely an id is fake or wrong and so this block fails at that point. I want to simple move on to the next id in the array and forget about the failed id. I don't even need to save an exception. I'm new to Ruby (coming from a PHP background).
I want to check if this syntax is correct. I am having trouble checking it in the rails console.
Syntax errors are easier to spot if the code is indented correctly.
if citation_array.blank?
flash.now[:error] = "There was a problem saving the publications selected!"
#user = current_user
render 'pubmed_search'
else
citation_array.each do |user_publication|
begin
publication = Publication.new
render_publication(user_publication)
publication.citation = user_publication
publication.user_id = current_user.id
publication.title = #title
publication.authors = #authors
publication.journal = #journal
publication.year = #year
publication.volume = #volume
publication.pages = #pages
if publication.save
next
end
rescue
next
end
end
#user = current_user
redirect_to current_user
return false
end
The syntax seems correct. Though an easier way to find out would have been just to run the code.
Some things in the code are not necessary though. After cleaning up your code a bit, it would look something like this with the same functionality.
#user = current_user
if citation_array.blank?
flash.now[:error] = 'There was a problem saving the publications selected!'
render 'pubmed_search'
else
citation_array.each do |user_publication|
begin
render_publication(user_publication)
Publication.create!( # create! here so that if something does go wrong, then you're not just ignoring it, but you can log it in your rescue block.
citation: user_publication,
user_id: current_user.id,
title: #title,
authors: #authors,
journal: #journal,
year: #year,
volume: #volume,
pages: #pages
# This hash should be extracted to a method.
)
rescue
# Just doing nothing here is valid syntax, but you should at least log your error.
end
end
redirect_to current_user
false # This can most likely be omitted as well since not many places care about the return value of a controller action.
end
Syntax for begin-rescue,
begin
your code...
rescue => e
Rails.logger.debug 'Exception is #{e}'
end
Here is our code to save quite a few objects all at once within one transaction. What the code does is to create a new checkout record (for warehouse) and update each item (there may be a few of them) in stock. Since all save has to be either all or none, we put all the save within Rails transaction:
#checkout = RequisitionCheckoutx::Checkout.new(params[:checkout])
#checkout.last_updated_by_id = session[:user_id]
#checkout.checkout_by_id = session[:user_id]
#checkout.transaction do
params['ids'].each do |id|
params['out_qtys'].each do |qty| #ids passed in as a string of 'id'
stock_item = RequisitionCheckoutx.warehouse_class.find_by_id(id.to_i)
qty = qty.to_i
if stock_item.stock_qty >= qty
stock_item.stock_qty = stock_item.stock_qty - qty
stock_item.last_updated_by_id = session[:user_id]
begin
stock_item.save
rescue => e
flash[:notice] = t('Stock Item#=') + id.to_s + ',' + e.message
end
end
end unless params['out_qtys'].blank?
end unless params['ids'].blank?
if #checkout.save
redirect_to URI.escape(SUBURI + "/authentify/view_handler?index=0&msg=Successfully Saved!")
else
flash[:notice] = t('Data Error. Not Saved!')
render 'new'
end
end
We haven't run the test yet and the code looks not pretty. Is there a better way to handle this kind of batch save? Also should the rescue loop be removed for transaction?
The transaction block should be performed first and then you should deal with action response. Besides that, catching exception here is pointless, cause using save returns simply true or false. Your transaction should look like:
RequisitionCheckoutx::Checkout.transaction do
begin
#...
#...
stock_item.save! # it will raise RecordInvalid or RecordNotSaved if something goes wrong
#...
#...
#checkout.save!
rescue Exception => e
raise ActiveRecord::Rollback # it calls Rollback to the database
end
end
Now, using ActiveModel::Dirty you need to check if #checkout has been saved:
if !#checkout.changed?
redirect_to "/something"
else
flash[:notice] = t('Data Error. Not Saved!')
render 'new'
end
I have a create method that calls a method in a model that pings some third-party APIs.
What I need to do is if the API sends back a certain message, then I'd display an error.
Below is my current controller and model setup, so how would I get the error back in to the controller (and ultimately the view)?
Here is the method in my controller:
def create
#number = Number.where(:tracking => params[:number][:tracking], :user_id => current_user.id).first
if #number.blank?
#number = Number.new
#number.tracking = params[:number][:tracking]
#number.user_id = current_user.id
#number.notes = params[:number][:notes]
#number.track
end
respond_with(#number) do |format|
format.html { redirect_to root_path }
end
end
Here are the methods in my model:
def track
create_events
end
def create_events(&block)
tracker = fedex.track(:tracking_number => number)
if tracker.valid?
self.assign_tracker(tracker)
tracker.events.each do |e|
self.create_event(e) unless (block_given? && !block.call(e))
end
save
else
# NEED TO THROW THE ERROR HERE
end
end
How about if rather than throwing errors, you just use validation? Something like the following (Just to get your started. This would need work.):
# if you don't cache the tracker in an attribute already, do this so
# you can add errors as if it were a column.
attr_accessor :tracker
def create_events(&block)
tracker = fedex.track(:tracking_number => number)
if tracker.valid?
# ...
else
# add the error with i18n
errors.add(:tracker, :error_type_if_you_know_it)
# or add it from a returned message
errors.add(:tracker, nil, :message => fedex.get_error())
end
end
Then in your controller:
#number.track
respond_with(#number) do |format|
if #number.errors.any?
format.html { redirect_to root_path }
else
format.html { render :some_template_with_errors }
end
end
Alternatively you could do this as part of validation (so calling valid? would work as expected and not destroy your custom "track" errors)
# do your tracking on creation, if number was given
validate :on => :create do
if number.present?
tracker = fedex.track(:tracking_number => number)
unless tracker.valid?
errors.add :tracker, nil, :message => tracker.get_error()
end
end
end
# then do your actual creation of tracking events sometime after validation
before_save :handle_tracker_assignment
def handle_tracker_assignment
self.assign_tracker(tracker)
# note the block method you're using would need to be reworked
# ...
end
Note in the latter case you'd have to change your logic a bit, and simply pass the tracking number and attempt to save a new record, which would trigger the tracking attempt.
You should typically offload the the API calls to a background job and you could either use notifiers (or Rack middleware) to raise self-defined errors and handle them accordingly.
How can I DRY the code below? Do I have to setup a bunch of ELSEs ? I usually find the "if this is met, stop", "if this is met, stop", rather than a bunch of nested ifs.
I discovered that redirect_to and render don't stop the action execution...
def payment_confirmed
confirm_payment do |confirmation|
#purchase = Purchase.find(confirmation.order_id)
unless #purchase.products_match_order_products?(confirmation.products)
# TODO notify the buyer of problems
return
end
if confirmation.status == :completed
#purchase.paid!
# TODO notify the user of completed purchase
redirect_to purchase_path(#purchase)
else
# TODO notify the user somehow that thigns are pending
end
return
end
unless session[:last_purchase_id]
flash[:notice] = 'Unable to identify purchase from session data.'
redirect_to user_path(current_user)
return
end
#purchase = Purchase.find(session[:last_purchase_id])
if #purchase.paid?
redirect_to purchase_path(#purchase)
return
end
# going to show message about pending payment
end
You can do the following to reduce the code.
1) Use
return redirect_to(..)
instead of
redirect_to(..)
return
2) Extract the flash and redirect_to code to a common method.
def payment_confirmed
confirm_payment do |confirmation|
#purchase = Purchase.find(confirmation.order_id)
return redirect_with_flash(...) unless #purchase.products_match_..(..)
return redirect_with_flash(...) unless confirmation.status == :completed
#purchase.paid!
return redirect_to(...)
end
return redirect_with_flash(...) unless session[:last_purchase_id]
#purchase = Purchase.find(session[:last_purchase_id])
return redirect_to(...) if #purchase.paid?
# going to show message about pending payment
end
Create a new method to redirect to a given url after showing a flash message.
def redirect_with_flash url, message
flash[:notice] = message
redirect_to(url)
end
Note I have truncated the code above in some places for readability.
Add and return false to the end of a redirect_to or render to halt execution at that point. That should help clean things up for you.
You could also factor out the steps into seperate methods. So the ending code would look something like:
def payment_confirmed
confirm_payment do |cnf|
confirmation_is_sane?(cnf) && purchase_done?(cnf)
return
end
has_last_purchase? && last_purchase_paid?
end
For a factoring looking like:
def confirmation_is_sane?(confirmation)
#purchase = Purchase.find(confirmation.order_id)
unless #purchase.products_match_order_products?(confirmation.products)
# TODO notify the buyer of problems and render
return false
end
true
end
def purchase_done?(confirmation)
if confirmation.status == :completed
#purchase.paid!
# TODO notify the user of completed purchase
redirect_to purchase_path(#purchase)
return false
else
# TODO notify the user somehow that thigns are pending and render
return true
end
end
def has_last_purchase?
unless session[:last_purchase_id]
flash[:notice] = 'Unable to identify purchase from session data.'
redirect_to user_path(current_user)
return false
end
#purchase = Purchase.find(session[:last_purchase_id])
return true
end
def last_purchase_paid?
if #purchase.paid?
redirect_to purchase_path(#purchase)
return false
end
# going to show message about pending payment
return true
end
This is basically just using true/falses with &&'s to do the early exiting rather than using return, but it seems to read easier, to me. You'd still have to call render in the other methods, but that shouldn't be too big of a deal.
The distinction between the confirmation order and the last purchase seems strange as well, but perhaps this is an artifact of the way confirm_payment works.