Rails / Stripe - undefined method `stripe_token' for nil:NilClass - ruby-on-rails

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.

Related

Rails - Ensuring an Event does not become over-subscribed

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.

How to fix this error "undefined method `encoding' for nil:NilClassa " and get canceling subscription plan worked?

This is my first time working with Stripe and Rails and now I am trying to allow premium users to cancel their subscriptions.
I can upgrade a user from standard level to premium level with my code, but I am having issues when I attempt to downgrade a premium user to the standard level.
I have followed Stripe Ruby API References of "Cancel a subscription": https://stripe.com/docs/api?lang=ruby#cancel_subscription, but I got this error when I clicked the "cancel subscription" button:
NoMethodError - undefined method encoding' for nil:NilClass:
/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/cgi/util.rb:7:inescape'
stripe (1.21.0) lib/stripe/list_object.rb:19:in retrieve'
app/controllers/subscriptions_controller.rb:55:indowngrade'
My rails version is 4.2.1.
My code:
class SubscriptionsController < ApplicationController
def create
subscription = Subscription.new
stripe_sub = nil
if current_user.stripe_customer_id.blank?
# Creates a Stripe Customer object, for associating with the charge
customer = Stripe::Customer.create(
email: current_user.email,
card: params[:stripeToken],
plan: 'premium_plan'
)
current_user.stripe_customer_id = customer.id
current_user.save!
stripe_sub = customer.subscriptions.first
else
customer = Stripe::Customer.retrieve(current_user.stripe_customer_id)
stripe_sub = customer.subscriptions.create(
plan: 'premium_plan'
)
end
current_user.subid = stripe_sub.id
current_user.subscription.save!
update_user_to_premium
flash[:success] = "Thank you for your subscription!"
redirect_to root_path
# Handle exceptions
rescue Stripe::CardError => e
flash[:error] = e.message
redirect_to new_subscriptions_path
end
def downgrade
customer = Stripe::Customer.retrieve(current_user.stripe_customer_id)
customer.subscriptions.retrieve(current_user.subid).delete
downgrade_user_to_standard
flash[:success] = "Sorry to see you go."
redirect_to user_path(current_user)
end
end
ApplitionController:
class ApplicationController < ActionController::Base
def update_user_to_premium
current_user.update_attributes(role: "premium")
end
def downgrade_user_to_standard
current_user.update_attributes(role: "standard")
end
end
config/initializers/stripe.rb:
Rails.configuration.stripe = {
publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
secret_key: ENV['STRIPE_SECRET_KEY']
}
# Set our app-stored secret key with Stripe
Stripe.api_key = Rails.configuration.stripe[:secret_key]
Any help will be appreciated!
Update:
Thanks for help from stacksonstacks, all I need is asserting 'subscription.user = current_user' under 'current_user.subid = stripe_sub.id', and then call subscription id with "subscription = current_user.subscription" in downgrade method. Now subscription cancelling works!
It seems current_user.subid returns nil on this line:
customer.subscriptions.retrieve(current_user.subid).delete
You assign subid for current_user but you never save the changes.
You only save the newly created subscription.
current_user.subid = stripe_sub.id
current_user.subscription.save!
If you add current_user.save! I think this will solve the problem.
Hope that helps

Dealing with a unique error

So I have got a database called Awards.
Users are able to 'award' a recipe but they can only do this once. The award database consists of recipe_id and user_id. I have made both of these columns unique so it wont allow you to award the recipe more than once. This works fine and if you attempt to award the recipe a second time I get this error:
columns user_id, recipe_id are not unique
Is there some code I can add into th create action to check for this error and then render a flash error message such as "already awarded recipe" instead of showing the error console?
this is my create method:
def create
#award = current_user.awards.build(award_params)
if #award.save
flash[:success] = "Award Given!"
redirect_to recipe_path(params[:recipe_id])
else
render 'static_pages/home'
end
end
Thanks,
Mike
There is a whole section of rails called validations that you're hitting on. The documentation is here: link. To get you basically set up, you could:
# award.rb
class Award < ActiveRecord::Base
validates_uniqueness_of :user_id, :recipe_id
end
# awards_controller.rb
def create
#award = current_user.awards.build(award_params)
if #award.save
flash[:success] = 'Award Given!'
redirect_to recipe_path(params[:recipe_id])
else
flash[:error] = 'There was an error awarding this award!'
render 'static_pages/home'
end
end

Advice on how to send email to multiple users based on an association

I have created a Ruby on Rails application where users can track workouts. The can do so either privately or publicly. On workouts which are public ( workout.share == 1 ) I allow users to comment. When a comment is created on a workout, the workout owner is notified via email. That all works great.
I am now looking for some advice on the best way to allow users who have commented on a workout, to also be notified via email. Here is an example.
User A creates Workout 1. User B comments on Workout 1 and User A receives an email notification. User C also comments on Workout 1 and both User A and User B receive email notifications.
What is the best way to tell my application to loop through all the users who have commented on Workout 1 and send an email to them?
Currently I am sending an email to the workout owner with the following code in the comments_controller (I realize this could be cleaner code):
class CommentsController < ApplicationController
...
def create
#workout = Workout.find(params[:workout_id])
#comment = #workout.comments.build(params[:comment])
#comment.user = current_user
respond_to do |format|
if #comment.save
if #comment.workout.email_notification == 1
#comment.deliver_comment_notification_mail!
format.html { redirect_to( projects_path) }
format.js
else
format.html { redirect_to( projects_path) }
format.js
end
else
end
end
end
...
and in comment_mailer.rb
def comment_notification_mail(comment)
subject "Someone commented on your Workout"
recipients("#{comment.workout.user.username} <#{comment.workout.user.email}>")
from("foobar")
body :comment => comment,
:commenter => comment.user,
:workout => comment.workout,
:commentee => comment.workout.user,
:workout_url => workout_url(comment.workout),
:commenter_url => user_url(comment.user)
end
To find out a workout owner and commenter is not a hard job. My suggestions are:
move the code of sending email in your controller to your model, using #after_create, eg:
class Comment < ActiveRecord::Base
#...
after_create :notify_subscribers
def subscribers
(self.workout.commenters << self.workout.owner).uniq
end
def notify_subscribers
#... implemented below
end
end
using delayed_job or other tools to put the email sending job to background, or the request would be blocked until all the emails has been sent. eg, in the #notify_owner_and_commenter method
def notify_subscribers
self.subscribers.each do |user|
CommentMailer.send_later :deliver_comment_notification_mail!(self, user)
end
end
Then you need to refactor you #deliver_comment_notification_mail! method with two arguments.
Delayed job ref: https://github.com/tobi/delayed_job
From my POV, it's all the work of the mailer. I'd just rewrite the comment_notification_mail to something more neutral (which could speak to workout owner and commenters).
Then something like:
def comment_notification_mail(comment)
recs = [comment.workout.user]
recs << comment.workout.comments(&:user)
recs -= comment.user
subject "Someone commented on your Workout"
recipients(recs.inject('') { |acc, r| "#{r.username} <#{r.email}>" })
from("foobar")
body :comment => comment,
:commenter => comment.user,
:workout => comment.workout,
:commentee => comment.workout.user,
:workout_url => workout_url(comment.workout),
:commenter_url => user_url(comment.user)
end
Of course, if mails are not supposed to be public, send by bcc ;)

Can I access information from one associated AR object in another when both are unsaved?

Say I open a Rails (2.3.8) script console and try this:
a = Account.new(:first_name) = 'foo'
i = a.invoices.build
p i.account.first_name
Account.rb is a model object and contains:
has_many :invoices
and Invoice.rb is a model as well containing:
belongs_to :account, :validate => true
In console line 3 above, i.account is nil. I realize that i.account would not be nil if account had been saved, but I do not wish to save an account unless I can create a valid invoice for the account. And, just for kicks, the invoice validation depends on some properties of the unsaved account.
Any ideas how to make this work?
Best,
Will
I typically do this with transactions. With rails transactions you can perform db interactions and roll them back at any time if something fails to validate. For example:
in your model:
def save_and_create_invoice
Account.transaction do
#first let's save the account, this will give us an account_id to work with
return false unless self.save
invoice = self.invoices.build
#setup your invoice here and then save it
if invoice.save
#nothing wrong? return true so we know it was ok
return true
else
#add the errors so we know what happened
invoice.errors.full_messages.each{|err| errors.add_to_base(err)}
#rollback the db transaction so the account isn't saved
raise ActiveRecord::Rollback
#return false so we know it failed
return false
end
end
end
And in your controller you would call it like so:
def create
#account = Account.new(params[:account])
respond_to do |format|
if #account.save_and_create_invoice
format.html
else
format.html {render :action => "new"}
end
end
end
Note that I didn't run this code to test it, just whipped it out real quick to show an example.

Resources