Why is my AASM state machine not triggered with Rails 7 Turbo patch links? - ruby

I just updated my Rails 6 app to Rails 7 and have problems updating my :patch and :delete links to Turbo.
For example, in one of my views I have this link...
link_to("Mark as sent", status_url(quote), :data => {:'turbo_method' => :patch})
... which is handled by this controller:
class StatusController < ApplicationController
def update
#quote = Quote.find(params[:id])
#quote.send_it! # Should trigger AASM
flash[:notice] = "Quote marked as sent."
redirect_to edit_quote_path(#quote)
end
end
In the model I am using AASM as a state machine:
class Quote < ApplicationRecord
include AASM
aasm :column => "status" do
state :draft, :initial => true
state :inquired
state :sent
state :downloaded
state :accepted
state :rejected
event :send_it do
transitions :from => [:draft, :inquired], :to => :sent
end
...
event :reset_it do
transitions :from => [:inquired, :sent, :downloaded, :accepted, :rejected], :to => :draft
end
end
end
The problem is that the state machine does not get triggered when I hit the link. The flash message and the redirect work but the state is not changed in the database. When I replace #quote.send_it! with #quote.update_column(:status, "sent")it works, however.
Can anybody tell me what I'm missing here?

I don't quite see how turbo is related. Except that I think your redirect isn't actually working:
Redirected to http://127.0.0.1:3000/quotes/1/edit
Completed 302 Found in 18ms (ActiveRecord: 4.3ms | Allocations: 7265)
Started PATCH "/quotes/1/edit" for 127.0.0.1 at 2022-08-12
ActionController::RoutingError (No route matches [PATCH] "/quotes/1/edit"):
# NOTE: ^ not quite a redirect
# v but it doesn't show on a page, it just refreshes the current one.
Started GET "/quotes" for 127.0.0.1 at 2022-08-12 17:51:28 -0400
# and if the current page were /quotes/1/edit then it would look like
# redirect worked, but I was submitting from /quotes.
Update your controller to actually show any errors:
def update
#quote = Quote.find(params[:id])
# NOTE: if transition fails, `send_it!` returns `false`
# (or raises an error for invalid transitions)
# when you run `#quote.update_column(:status, "sent")`
# validations and state machine are not triggered and it works.
if #quote.send_it!
flash.notice = "Quote marked as sent."
else
flash.notice = #quote.errors.full_messages.join(", ")
end
respond_to do |format|
# in case you want add a stream response later
# format.turbo_stream { # TODO }
format.html { redirect_to edit_quote_path(#quote), status: :see_other }
# NOTE: Redirect as a GET request instead of PATCH ^
end
end
Or just add whiny_persistence flag and check the logs, this will raise validation errors:
aasm column: :status, whiny_persistence: true do

Not sure where you got the mark_as_ from, change that to #quote.aasm.fire! status.
Edit
Sorry, not status, needs to be the event, just use the right event.

Related

How to test Stripe's invoice_pdf property when it keeps changing?

In my Rails 6 app I have a very simple controller that displays download links to a user's Stripe invoice PDFs:
class ReceiptsController < ApplicationController
before_action :signed_in_user
def index
receipts = current_account.receipts
end
def show
receipt = current_account.receipts.find(params[:id])
stripe_invoice = Stripe::Invoice.retrieve(receipt.stripe_invoice_id)
redirect_to stripe_invoice.invoice_pdf
end
end
Since Stripe doesn't provide permanent invoice URLs (please correct me if I am wrong), I am storing each invoice's Stripe ID in the database and then use that ID to lookup the current URL to the invoice PDF from the Stripe API.
The problem is that this works most of the time but not all the time. The spec that I created for the controller show action fails in about 20 % of cases because the two URLs do not match:
describe ReceiptsController, :type => :controller do
before :each do
#account = FactoryBot.create(:activated_account)
#user = #account.users.create(FactoryBot.attributes_for(:user))
sign_in(#user)
end
describe 'GET #show' do
# The implementation details of this block don't really matter
before :each do
Customers::FindOrCreate.call(#account)
stripe_subscription = Subscriptions::CreateRemote.call(#account,:payment_behavior => "default_incomplete")
#stripe_invoice = stripe_subscription.latest_invoice
#receipt = Receipts::Create.call(#stripe_invoice)
end
# This test fails in about 20 % of cases because the redirect does not go to #stripe_invoice.invoice_pdf but a slightly different URL
it "redirects to Stripe invoice PDF" do
get :show, :params => {:id => #receipt}
expect(response).to redirect_to #stripe_invoice.invoice_pdf
end
end
end
How can this be? Does the invoice_pdf property of a Stripe invoice change every few seconds? I've been trying to work this out for days now but can't get my head around it.
Addition:
This is a typical test failure that I get quite often:
Expected response to be a redirect to <https://pay.stripe.com/invoice/acct_105jfm2HzYSlmhv7/test_YWNjdF8xMDJqc20yS3pZUmxzaHc0LF9NMGZONnFzNUpPTjlObVprd0hvdGpIdWFUamJHTTVxLDQ3Njc3MDY30200oOxX3A1/pdf?s=ap> but was a redirect to <https://pay.stripe.com/invoice/acct_105jfm2HzYSlmhv7/test_YWNjdF8xMDJqc20yS3pZUmxzaHc0LF9NMGZONnFzNUpPTjlObVprd0hvdGpIdWFUamJHTTVxLDQ3Njc3MDY402001iYCSUbn/pdf?s=ap>.
Expected "https://pay.stripe.com/invoice/acct_105jfm2HzYSlmhv7/test_YWNjdF8xMDJqc20yS3pZUmxzaHc0LF9NMGZONnFzNUpPTjlObVprd0hvdGpIdWFUamJHTTVxLDQ3Njc3MDY30200oOxX3A1F/pdf?s=ap" to be === "https://pay.stripe.com/invoice/acct_105jfm2HzYSlmhv7/test_YWNjdF8xMDJqc20yS3pZUmxzaHc0LF9NMGZONnFzNUpPTjlObVprd0hvdGpIdWFUamJHTTVxLDQ3Njc3MDY402001iYCSUbn/pdf?s=ap".

How to display payment gateway response messages in view - Rails 4, Active Merchant

I'm using Active Merchant with Stripe as the payment gateway. Everything works fine except that i don't know how to go about getting the gateway response error messages from Stripe (when a card is declined, invalid etc) to display on the checkout page to the user. I can get a StandardError to be raised that redirects to an error page with the response message but that's it.
ORDER MODEL
class Order < ActiveRecord::Base
has_many :order_products
has_many :products, through: :order_products
attr_accessor :card_number, :security_code, :card_expires_on
validate :validate_card, :on => :create
def validate_card
unless credit_card.valid?
credit_card.errors.full_messages.each do |message|
errors[:base] << message
end
end
end
def purchase(basket)
response = GATEWAY.purchase(Product.total_basket_price(basket)*100, credit_card, purchase_options)
unless response.success?
raise StandardError, response.message
end
end
def credit_card
#credit_card ||= ActiveMerchant::Billing::CreditCard.new(
:number => card_number,
:first_name => first_name,
:last_name => last_name,
:verification_value => security_code,
:month => card_expires_on.month,
:year => card_expires_on.year
)
end
def purchase_options
{
:billing_address => {
:address1 => address_1,
:address2 => address_2,
:city => city,
:country => country_code,
:zip => postal_code
}
}
end
end
ORDERS CONTROLLER
class OrdersController < ApplicationController
def create
#order = Order.new(order_params)
# #product = basket.find(params[:product_id])
basket.each do |item_id|
#order.order_products.build(product: Product.find(item_id))
end
if #order.save
if #order.purchase(basket)
render "show"
else
render "failure"
end
else
render "new"
end
end
Can anyone lend a hand, please??
Many Thanks
Easy peasy!
This is a simple matter of control flow. In Ruby, as in most languages, exceptions interrupt the normal program flow. As your code is written now, #purchase is raising an exception when it fails.
That's fine and a perfectly valid design decision. But the code interacting with #purchase is this:
if #order.purchase(basket)
render "show"
else
render "failure"
end
That code has no exception handling, so any exception will be caught by Rails, program flow will halt, and you'll get either a detailed error page (in development mode) or a generic 500 error page (in production mode).
Since you profess to be new to Ruby and Rails, a little code substitution might make this clearer:
# If #purchase is successful, it evaluates to true.
if true
render "show" # 'show' view is rendered as expected. Flow stops.
else
render "failure"
end
# If #purchase fails, it raises an exception.
if raise StandardError, response.message
# ^^^ Exception is raised, flow stops here.
render "show" # This code is never reached.
else # This code is never reached.
render "failure" # This code is never reached.
end
As I implied in the beginning, though, it's an easy fix once you know what the issue is. You can simply handle the exception with rescue. Where you currently have an if/else block, you can swap in an if block and a rescue block:
if #order.purchase(basket)
render 'show'
end
rescue => e
render 'failure'
There's room for improvement here depending on your needs—since you're raising and rescuing StandardError, for example, your can't easily distinguish between a network failure and a declined card—but it'll get you moving again.
After a lot of fiddling and help, the working solution was to search for an error key within the response params hash and if an error was present add the message to the object errors. Not particularly elegant but it now does what i want.
ORDER MODEL
def purchase(basket)
response = GATEWAY.purchase(Product.total_basket_price(basket)*100, credit_card, purchase_options)
if response.params.key?('error')
self.errors.add :base, response.message
false
else
true
end
end
ORDERS CONTROLLER
Also switched the order of the if statements in the controller so that def purchase(basket) runs first before the order is saved, allowing the error message(s) from the response to be caught and displayed.
if #order.purchase(basket)
if #order.save
render "show"
else
render "new"
end
else
render "new"
end
VIEW
<%= if #order.errors.any?
#order.errors[:base].to_sentence
end%>

nil method error on omniauth.auth

def create
5 debugger
6 auth=request.env["omniauth.auth"]
=> 7 user=Moviegoer.find_by_provider_and_uid(auth["provider"],auth["uid"]) ||
8 Moviegoer.create_with_omniauth(auth)
9 session[:user_id] = user.id
10 redirect_to movies_path
The above code is from the controller action (create) - i ran with debugger on;
I am getting 'nil' value for auth variable - i had omniauth in gem file and bundle installed it...still not able to get the above statement executed right...am i missing something here...?
------
I tried few things and progressed few steps -but still stuck at an error
Started GET "/auth/twitter" for 127.0.0.1 at 2013-12-28 17:38:26 -0800
Timeout::Error (execution expired):
The code in application controller is :
class ApplicationController < ActionController::Base
before_filter :set_current_user
protected # prevents method from being invoked by a route
def set_current_user
debugger
# we exploit the fact that find_by_id(nil) returns nil
#current_user ||= Moviegoer.find_by_id(session[:user_id])
redirect_to '/auth/twitter' and return unless #current_user
end
end
I believe the code is timing out at redirect_to statement...
You should map your oauth callback to your create action.
# config/routes.rb
match '/auth/:provider/callback' => 'authentications#create'

cramp framework sync 'render' correct way using em-synchrony

To describe my problem I attach simple Cramp http://cramp.in/ class.
I add some modification but its mainly work like https://github.com/lifo/cramp-pub-sub-chat-demo/blob/master/app/actions/chat_action.rb
class ChatAction < Cramp::Websocket
use_fiber_pool
on_start :create_redis
on_finish :handle_leave, :destroy_redis
on_data :received_data
def create_redis
#redis = EM::Hiredis.connect('redis://127.0.0.1:6379/0')
end
def destroy_redis
#redis.pubsub.close_connection
#redis.close_connection
end
def received_data(data)
msg = parse_json(data)
case msg[:action]
when 'join'
handle_join(msg)
when 'message'
handle_message(msg)
else
# skip
end
end
def handle_join(msg)
#user = msg[:user]
subscribe
publish(:action => 'control', :user => #user, :message => 'joined the chat room')
end
def handle_leave
publish :action => 'control', :user => #user, :message => 'left the chat room'
end
def handle_message(msg)
publish(msg.merge(:user => #user))
# added only for inline sync tests
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
private
def subscribe
#redis.pubsub.subscribe('chat') do |message|
render(message)
end
end
def publish(message)
#redis.publish('chat', encode_json(message))
end
def encode_json(obj)
Yajl::Encoder.encode(obj)
end
def parse_json(str)
Yajl::Parser.parse(str, :symbolize_keys => true)
end
def render_json(hash)
render encode_json(hash)
end
end
More about what i try to do is in handle_message method.
I try send messages to client in correct order. First publish message to all subscribers, second render some internal info only for current connected client.
For above code client receives:
{"action":"message","user":"user1","message":"this info should appear after published message"}
{"action":"message","message":"simple message","user":"user1"}
Its not synchronized, because of em-hiredis defferable responses, probably.
So I try to synchronized it this way:
def handle_message(msg)
EM::Synchrony.sync publish(msg.merge(:user => #user))
EM::Synchrony.next_tick do # if I comment this block messages order is still incorrect
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
end
Now, client handle messages with correct order.
{"action":"message","message":"simple message","user":"user1"}
{"action":"message","user":"user1","message":"this info should appear after published message"}
My questions are:
When I comment EM::Synchrony.next_tick block, messages order is still incorrect. What meaning have EM::Synchrony.next_tick block in this example?
Is this good way to handle inline sync with Cramp or EventMachine ?
Is there a better, clearer way to handle it ?
Thank you!
I found solution of this problem, em-synchrony should work inline out of the box by requiring this library:
require 'em-synchrony/em-hiredis'
class ChatAction < Cramp::Websocket
Using EM::Synchrony.next_tick block is bad idea, with big help of em-synchrony community I add em-hiredis 0.2.1 compatibility patch on github
So now handle_message method looks like this:
def handle_message(msg)
publish(msg.merge(:user => #user))
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
Don`t forget to take this gem from github
gem 'em-synchrony', :git=> 'git://github.com/igrigorik/em-synchrony.git'
Hope it helps someone.

state_machine using after_failure with ActiveRecord transactions

I'm using ruby state_machine to manage processing documents. As the document progresses the SM states manage the extra validations needed.
If any step fails, such as the document being the wrong dimensions, I want to record this so I can inform a user who is waiting for the conversion to complete.
My plan was to use SM after_failure to fill in a failed_at timestamp in the model. The polling user interface would see the processing has failed and display an appropriate message.
My issue is that if a transition fails it rolls back the transaction including the after_failure method. This means the user never finds out that the processing failed and unfortunately sits in front of their screen indefinitely.
In the example below I want the :fail_transaction method to run despite the transition failing. I'm open to any ideas or alternate ways to notify the user of failure.
Notes:
The :use_transaction => false option for state machine doesn't seem
to make a difference.
I considered putting the fail conversion into a worker queue to escape the transaction but as I'm using delayed_job so queue entries are also wound back.
This project is shamefully still on Rails 2.3.x & ruby 1.8.x
Example code:
state_machine :state, :initial => :new do
state :new
state :original_uploaded do
validates_presence_of :original_file
end
state :original_converted do
validates_presence_of :pdf
end
state :conversion_complete do
validates :pdf_dimensions_correct
end
event :process do
transition :new => :original_uploaded
transition :original_uploaded => :original_converted
transition :original_converted => :conversion_complete
end
after_transition :on => :process, :do => :process
before_transition any => :original_converted, :do => :convert_original_to_pdf
before_transition any => :conversion_complete, :do => :extract_pdf_metadata
after_failure :on => :process, :do => :fail_conversion
end
def fail_conversion
update_attribute(:failed_at, Time.now) # Should update dispite being invalid state
end
def convert_original_to_pdf
#...
end
def extract_pdf_metadata
#...
end
def pdf_dimensions_correct
errors.add(...)
end
Thanks in advance!

Resources