I'm trying to write a registration using devise and active merchant. The form is complex in that my user object looks like this:
class User < ActiveRecord::Base
include ActiveMerchant::Utils
# Include default devise modules. Others available are:
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :omniauthable
# Setup accessible (or protected) attributes
attr_accessible :email, :password, :password_confirmation, :remember_me, :username, :first_name,
:subscription_attributes, :last_name, :zipcode,
:payment_profile_attributes, :customer_cim_id, :payment_profile_id
...
# Track multi-page registration
attr_writer :current_step
...
# Setup Payment Profile element (authorize.net billing profile)
has_one :payment_profile, :dependent => :delete
accepts_nested_attributes_for :payment_profile
Now the PaymentProfile class has its own children, two items from active merchant:
require 'active_merchant'
class PaymentProfile < ActiveRecord::Base
include ActiveMerchant::Billing
include ActiveMerchant::Utils
validate_on_create :validate_card, :validate_address
attr_accessor :credit_card, :address
belongs_to :user
validates_presence_of :address, :credit_card
def validate_address
unless address.valid?
address.errors.each do |error|
errors.add( :base, error )
end
end
end
def address
@address ||= ActiveMerchant::Billing::Address.new(
:name => last_name,
:address1 => address1,
:city => city,
:state => state,
:zip => zipcode,
:country => country,
:phone => phone
)
end
def validate_card
unless credit_card.valid?
credit_card.errors.full_messages.each do |message|
errors.add( :base, message )
end
end
end
def credit_card
@credit_card ||= ActiveMerchant::Billing::CreditCard.new(
:type => card_type,
:number => card_number,
:verification_value => verification_code,
:first_name => first_name,
:last_name => last_name
)
@credit_card.month ||= card_expire_on.month unless card_expire_on.nil?
@credit_card.year ||= card_expire_on.year unless card_expire_on.nil?
return @credit_card
end
Now I've overrided the RegistrationsController from Devise to handle the multi-page form using the solution from Ryan Bates multi-page form screencast (http://railscasts.com/episodes/217-multistep-forms). I had to tweak it a bit to get it working with Devise, but I was successful. Now because Ryan's multi-page form simply asked for different fields from the same model on different pages, he was able to override his valid?
method by adding an :if block to his validate method a la:
validates_presence_of :username, :if => lambda { |o| o.current_step == "account" }
But in my case, I'm asking for all the fields on the first form from my parent model (User), and then asking for the all the fields from my two grandchild models (User:PaymentProfile:Address, User:PaymentProfile:Credit_Card) on teh second page.
The problem I'm facing is that although PaymentProfile.valid? returns errors based on ActiveMerchant's logic, the form itself doesn't render or even display those errors. The view code for the billing page looks like this:
<h2>Payment Details</h2>
<%= semantic_form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<%= f.semantic_fields_for :payment_profile do |p| %>
<%= p.semantic_fields_for :address do |a| %>
<%= a.inputs "Billing Information", :id => "billing" do %>
<%= a.input :type, :label => "Credit Card", :as => :select, :collection => get_creditcards %>
<%= a.input :number, :label => "Card Number", :as => :numeric %>
<%= a.input :card_expire_on, :as => :date, :discard_day => true, :start_year => Date.today.year, :end_year => (Date.today.year+10), :add_month_numbers => true %>
<%= a.input :first_name %>
<%= a.input :last_name %>
<%= a.input :verification_code, :label => "CVV Code" %>
<% end %>
<% end %>
<%= f.semantic_fields_for :credit_card do |c| %>
<%= c.inputs "Billing Address", :id => "address" do %>
<%= c.input :address1, :label => "Address" %>
<%= c.input :city %>
<%= c.input :state, :as => :select, :collection => Carmen::states %>
<%= c.input :country, :as => :select, :collection => Carmen::countries, :selected => 'US' %>
<%= c.input :zipcode, :label => "Postal Code" %>
<%= c.input :phone, :as => :phone %>
<% end %>
<% end %>
<% end %>
<%= f.commit_button :label => "Continue" %>
<% unless @user.first_step? %>
<%= f.commit_button :label => "Back", :button_html => { :name => "back_button" } %>
<% end %>
<% end %>
I added a puts errors
message in my code right after the valid? command and it shows as follows:
{:base=>[["first_name", ["cannot be empty"]], ["last_name", ["cannot be empty"]], ["year", ["expired", "is not a valid year"]], ["type", ["is required", "is invalid"]], ["number", ["is not a valid credit card number"]], ["verification_value", ["is required"]], ["address1", ["is required"]], ["city", ["is required"]], ["state", ["is required"]], ["zip", ["is required", "must be a five digit number"]], ["phone", ["is required", "must be in the format of 333-333-3333"]]]}
{:base=>[["first_name", ["cannot be empty"]], ["last_name", ["cannot be empty"]], ["year", ["expired", "is not a valid year"]], ["type", ["is required", "is invalid"]], ["number", ["is not a valid credit card number"]], ["verification_value", ["is required"]], ["address1", ["is required"]], ["city", ["is required"]], ["state", ["is required"]], ["zip", ["is required", "must be a five digit number"]], ["phone", ["is required", "must be in the format of 333-333-3333"]]]}
Now the structure of this output doesn't match the output of a standard error output which is built off a single layer hash such as:
{:username=>["can't be blank"]}
So after showing you all of that, my questions are these: a) how do I get the error output to show properly so that the form actually spits them out? b) how do I prevent the parent.valid? from also validating the grandchildren.valid? when I'm not on that page? I can't use the :if => lambda... solution on child models because they don't know what the current_step is.
My apologies for such a long post, I just wanted to include as much information as possible. I've been wrestling with this for a week now and I can't seem to get past it. Any advice would be hugely helpful. Thanks in advance.
The reason the errors aren't showing is probably that they are populated on the base, not on the individual attributes. This happens in your validate_card
and validate_address
methods. Instead of adding errors to base, you should add them to the specific attribute that caused the error.
errors.add( attr , error )
Secondly, if you want to make your validations dependent on a certain state, as the screencast you mentioned, then you need to save the state flag with the model (probably best the parent). You can do this by hand or, better, you can use a gem for this (recommended): state_machine
Good luck.
On a high level, you seem to be using inheritance in your object modeling and this model is getting built in several forms, in almost 'wizard' like approach. My suggestion would be to model your objects to reflect, the actual forms like,
First part of the form collect basic User information : UserInformation model
Second Part of the form collect payment related information: PaymentInformation model (the Active merchant stuff)
and so on...
Where either the User model has one UserInformation, has one PaymentInformation and so on.
Essentially replace inheritance with Composition. Try and see if you can avoid extending the ActiveMerchant frame work too.
The above style, will give you more control over when you want to call #valid? on a subset of you data model. It get constructed part by part as the user move through the form.
Sorry, I dont have specific solution for you but a more general rewrite approach.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With