Stack Level Too Deep Error In Rails - ruby-on-rails

I'm getting this error in Rails and there's no other information to help me figure out why.
SystemStackError in UserController#students
stack level too deep
My user controller is as follows
class UserController < ApplicationController
before_filter :authenticate_user!, :except => [:show, :students, :mentors]
...
def students
#users = Student.where(:verify_code => 'VERIFIED')
end
end
Even when I remove the #users line the error persists.
EDIT: The error seems to happen to every single route aside from the devise_for :users and to_root, which means just the User controller.
User Model
require 'json'
require 'socket'
class User < ActiveRecord::Base
serialize :roles
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
validates :ign, :server, :roles, :presence => true
validate :summoner_exists
validate :valid_roles
attr_accessible :email, :password, :password_confirmation, :remember_me, :ign, :server, :tier, :roles, :type
def valid_roles
valid_role_array = ['Top', 'Mid', 'Jungle', 'AD Carry', 'Support']
self.roles.each do |role|
unless valid_role_array.include?(role)
self.roles.delete(role)
end
end
end
def summoner_verified?
return self.verify_code == 'VERIFIED'
end
def summoner_verify
rune_pages = shurima_api(self.server, 'rune_pages', self.acctid)
unless rune_pages
return false
else
rune_pages.each do |page|
if (page['name'] == self.verify_code)
self.verify_code = 'VERIFIED'
self.save
return true
end
end
end
return false
end
def summoner_exists
json = shurima_api(self.server, 'summoner', self.ign)
unless json
errors.add(:ign, "The summoner name \"#{self.ign}\" doesn't exist on #{self.server}")
else
self.summonerid = json['summonerId']
self.acctid = json['acctId']
self.verify_code = Array.new(10){rand(36).to_s(36)}.join
eligible_to_mentor
end
end
def eligible_to_mentor
leagues = shurima_api(self.server, 'leagues', self.summonerid)
unless leagues
errors.add(:ign, "That summoner doesn't seem to meet the requirements to become a mentor. Make sure you're at least in a Platinum League")
return false
end
leagues.each do |league|
if (league['queue'] == 'RANKED_SOLO_5x5')
self.tier = league['tier']
end
end
eligible_tiers = ['PLATINUM', 'DIAMOND', 'CHALLENGER']
if (self.type == 'Mentor' && !eligible_tiers.include?(self.tier))
errors.add(:mentor, "Mentors must be at least PLATINUM.")
end
end
def shurima_api(server, method, args)
host, port = 'ip removed', 714
TCPSocket.open(host, port) do |socket|
ready = IO.select([socket], [socket], nil, 8)
return false unless ready
socket.puts server + "&" + method + "&" + args.to_s
message = socket.gets.chomp
if message == '"Unknown error"'
return false
end
return JSON.parse(message)
end
end
end

I ended up fixing it by renaming the request method to request_mentor because it was causing the infinite loop.

Related

undefined method `__metadata' for #<Participant:0x00000001076da378> with rails 6 / mongoid

I have the follow code that is working in rails 5. Updagrate to 6 I get the error undefined method `__metadata'.
Here's the problematic code
*
def nature
self.__metadata.key.to_s.singularize.to_sym #
end
*
Have try to use method but it doesn't return what it does in rails 5 / mongoid. Mongoid version is '~> 7.0'
Complete class code
# Participant model class definition
class Participant
include Mongoid::Document
include Mongoid::Timestamps
include DryValidation
field :address
field :identifier
field :name
field :birthdate, type: Date
field :sex
field :profession
field :phone
field :email
field :ownership_percentage
field :contribution_amount
field :category
field :group
field :registered_on, type: Date
field :retired, type: Boolean
field :retired_on, type: Date
field :committee
# Callbacks
before_save :generate_identifier
# Relations
embedded_in :book, inverse_of: :shareholders
embedded_in :book, inverse_of: :directors
embedded_in :book, inverse_of: :employees
embedded_in :book, inverse_of: :committee_members
embeds_many :participant_files
accepts_nested_attributes_for :participant_files, allow_destroy: true
#Validations
validates :name, presence: true
validates :email, allow_blank: true, format: { with: /\A\S+#\S+\.\S+\z/i }
validates :registered_on, presence: true, non_existent_date: true
validates :birthdate, non_existent_date: true
validates :retired_on, non_existent_date: true
validate :registered_on_date
def self.options_for(field_name)
case field_name.to_sym
when :category then [nil, :founders, :actives, :participants]
when :sex then [nil, :male, :female]
when :group then [nil, :legal, :accounting, :human_resources, :consumer, :employee,
:management_and_administration, :communication_and_marketing,
:ethic_and_gouvernance, :other]
else []
end
end
def self.ordered
# This should be as simple as .order_by(:retired_on.desc, :registered_on.asc)
# but the registered_on parameters is never ordered correctly so I had to do this ugly thing :(
self.all.sort_by{ |a| [ (a.retired_on ? a.retired_on.strftime('%Y%m%d') : 0), (a.registered_on ? a.registered_on.strftime('%Y%m%d') : 0) ].join }
end
def self.ordered_by_name
participants = self.active.sort_by{ |p| p.name.downcase }
participants += self.inactive.sort_by{ |p| p.name.downcase }
participants
end
def self.active
now = Time.now.strftime('%Y%m%d')
self.all.select do |a|
if a.registered_on
if a.retired_on
a.retired_on.strftime('%Y%m%d') >= now && a.registered_on.strftime('%Y%m%d') <= now
else
a.registered_on.strftime('%Y%m%d') <= now
end
end
end
end
def self.inactive
now = Time.now.strftime('%Y%m%d')
self.all.select do|a|
(a.retired_on && a.retired_on.strftime('%Y%m%d') < now) ||
(a.registered_on && a.registered_on.strftime('%Y%m%d') > now)
end
end
def book
self._parent
end
def committee_member?
self.nature == :committee_member
end
def director?
self.nature == :director
end
def employee?
self.nature == :employee
end
def nature
self.__metadata.key.to_s.singularize.to_sym #
end
def active?
!retired?
end
def retired?
self.retired_on && self.retired_on <= Time.zone.today
end
def shareholder?
self.nature == :shareholder
end
def securities
self.book.transactions.any_of({from: self.id}, {to: self.id}).asc(:transacted_on)
end
def save_files
self.participant_files.each do |pf|
pf.save
end
delete_objects_without_file
end
def has_shares?
book.share_categories.each do |sc|
return true if total_shares(sc) > 0
end
false
end
def total_shares(share_category)
total = 0
securities.each do |s|
if s.share_category == share_category
if s.nature == 'issued' or (s.nature == 'transfered' and self.id.to_s == s.to.to_s)
total += s.quantity if s.quantity
elsif s.nature == 'repurchased' or (s.nature == 'transfered' and self.id.to_s == s.from.to_s)
total -= s.quantity if s.quantity
end
end
end
total
end
def share_class_percentage(sc)
book.share_class_quantity(sc) > 0 ? self.total_shares(sc)/book.share_class_quantity(sc).to_f*100 : 0
end
def acceptance_documents
self.book.documents.select{|document| document.participant_id == self.id && document.nature == 'dir_accept'}
end
def resignation_documents
self.book.documents.select{|document| document.participant_id == self.id && document.nature == 'dir_resig'}
end
private
def existing_identifier?
participant_type = self.__metadata.key.to_sym
identifiers = book.send(participant_type).map{ |p| p.identifier if p.id != self.id }.compact
identifiers.include? self.identifier
end
def generate_identifier
self.identifier = self.name.parameterize if self.identifier.blank?
i = 1
while existing_identifier?
self.identifier = "#{self.identifier}-#{i}"
i += 1
end
end
def registered_on_date
unless registered_on.nil? || retired_on.nil?
if registered_on > retired_on
errors.add(:registered_on, I18n.t("mongoid.errors.models.participant.attributes.registered_on.greater_than_retired_on"))
end
end
end
def delete_objects_without_file
self.participant_files.each do |pf|
pf.delete if pf.pdf_file.file.nil?
end
end
end```

Upgrading from Rails 4 to Rails 6 and handling Nested Parameters

I am helping a client upgrade his application from Rails 4 to Rails 6, and one of his model objects isn't saving properly and I'm having trouble figuring out why. I think the main trouble is figuring out how to set up the params.permit based on a somewhat unorthodox parameter structure. The main problem at this point is that the nested parameter frequency isn't attaching to the service_agreement even though the data appears to be coming through in the parameters correctly. I would think that redefining the strong parameters would provide a solution, but I'm not sure if that's true.
From the Model
# Model object
class ServiceAgreement < ApplicationRecord
has_one :frequency, dependent: :destroy
accepts_nested_attributes_for :frequency, reject_if: :reject_frequency
end
From the Controller
def create
#service_agreement = ServiceAgreement.new(service_agreement_params)
authorize #service_agreement
if params[:service_agreement][:start_date].present?
#service_agreement.start_date = #service_agreement.set_proper_date(params[:service_agreement][:start_date])
end
if params[:first] && params[:second] && params[:third]
#service_agreement.phone_number = params[:first] + params[:second] + params[:third]
end
if params[:service_agreement][:end_date].present?
#service_agreement.end_date = #service_agreement.set_proper_date(params[:service_agreement][:end_date])
end
if params[:location_id] && params[:location_id] == "master"
#service_agreement.master = true
elsif params[:location_id]
#location = Location.find(params[:location_id])
#service_agreement.location_id = #location.id
end
if #service_agreement.save
redirect_to pick_services_service_agreement_path(#service_agreement), notice: "Service Agreement Successfully Created"
else
#account = #service_agreement.account
#chosen_invoice_frequency = #service_agreement.try(:invoice_frequency)
#default_invoice_frequency = #account.try(:invoice_frequency)
#service_agreement.build_frequency unless #service_agreement.frequency.present?
respond_with(#service_agreement)
end
end
Here are the paramters as they come through from the form:
Parameters: {"authenticity_token"=>"xxx", "service_agreement"=>{"active_status"=>"active", "service_type"=>"on_site_shredding", "job_type"=>"recurring", "sa_name"=>"", "contact_name"=>"", "location_id"=>"45729", "department"=>"", "frequency_attributes"=>{"day_count"=>"", "week_count"=>"1", "month_count"=>"", "day_on"=>"", "date_on"=>"", "week_on"=>"", "once"=>"false", "second_day_on"=>"", "second_date_on"=>"", "second_week_on"=>"", "on_demand"=>"false", "third_day_on"=>"", "fourth_day_on"=>"", "fifth_day_on"=>"", "sixth_day_on"=>""}, "start_date"=>"01/06/2021", "end_date"=>"06/23/2021", "estimated_service_time"=>"15", "notes"=>"", "standard_minimum"=>"0.0", "next_day_minimum"=>"0.0", "same_day_minimum"=>"0.0", "rush_minimum"=>"0.0", "invoice_frequency"=>"immediate", "po_number"=>"", "taxable"=>"0", "tax_rate_id"=>"33", "organization_id"=>"15", "account_id"=>"41447"}, "same_info"=>"1", "first"=>"727", "second"=>"714", "third"=>"7750", "commit"=>"Save"}
Originally, the strong parameters were defined as follows:
def service_agreement_params
params.require(:service_agreement).permit(:account_id, :location_id, :organization_id, :minimum_storage, :minimum_delivery, :standard_minimum, :rush_minimum, :next_day_minimum, :same_day_minimum, :payment_method, :service_type, :active, :contact_name, :estimated_service_time, :phone_number, :notes, :department, :po_number, :invoice_frequency, :active_status, :taxable, :tax_rate_id, :sa_name, :job_type, frequency_attributes: [:id, :day_count, :week_count, :month_count, :day_on, :second_day_on, :third_day_on, :fourth_day_on, :fifth_day_on, :sixth_day_on, :date_on, :second_date_on, :week_on, :second_week_on, :service_agreement_id, :once, :on_demand])
end
I have redefined the strong parameters to cut down on unpermitted errors as follows:
def service_agreement_params
params.permit(:start_date, :end_date, {
service_agreement: [
:account_id,
:location_id,
:organization_id,
:minimum_storage,
:minimum_delivery,
:standard_minimum,
:rush_minimum,
:next_day_minimum,
:same_day_minimum,
:payment_method,
:service_type,
:active,
:contact_name,
:estimated_service_time,
:phone_number,
:notes,
:department,
:po_number,
:invoice_frequency,
:active_status,
:taxable,
:tax_rate_id,
:sa_name,
:job_type,
{ frequency_attributes: [
:id,
:day_count,
:week_count,
:month_count,
:day_on,
:second_day_on,
:third_day_on,
:fourth_day_on,
:fifth_day_on,
:sixth_day_on,
:date_on,
:second_date_on,
:week_on,
:second_week_on,
:service_agreement_id,
:once,
:on_demand
] }
]
})
end
EDIT: Adding Frequency model based on comment
class Frequency < ApplicationRecord
belongs_to :service_agreement
validate :first_day_if_second
validate :first_date_if_second
validate :first_day_before_second
validate :second_week_after_first
validate :day_on_if_week_on
validate :second_date_on_after_first
validate :week_on_and_date_on_has_month
validate :date_between_1_to_30
validate :second_date_between_1_to_30
private
def first_day_if_second
if second_day_on.present? && !day_on.present?
errors.add(:day_on, "You must add a first day if you have a second.")
end
end
def first_day_before_second
if second_day_on.present? && day_on.present? && day_on >= second_day_on && second_week_on == nil
errors.add(:second_day_on, "Your second day must be later in the week than the first.")
end
end
def second_week_after_first
if second_week_on.present? && week_on.present? && second_week_on <= week_on
errors.add(:second_week_on, "Your second week for service for your frequency must be after the first")
end
end
def day_on_if_week_on
if week_on.present? && !day_on.present?
errors.add(:week_on, "You must select a day on if you select a week")
end
end
def second_date_on_after_first
if second_date_on.present? && date_on.present? && second_date_on <= date_on
errors.add(:second_date_on, "Your second date must be after the first.")
end
end
def first_date_if_second
if second_date_on.present? && !date_on.present?
errors.add(:second_date_on, "You must have a first date if you have a second")
end
end
def week_on_and_date_on_has_month
if (week_on.present? || date_on.present?) && month_count == nil
errors.add(:month_count, "You must have a month if you have weeks or dates selected")
end
end
def date_between_1_to_30
if date_on.present? && !date_on.between?(1, 30)
errors.add(:date_on, "Your date for your frequency must be between 1 and 30")
end
end
def second_date_between_1_to_30
if second_date_on.present? && !second_date_on.between?(1, 30)
errors.add(:second_date_on, "Your second date for your frequency must be between 1 and 30")
end
end
end

Restrict the chance to delete a booking just more than x hours before departure in Rails

I want users not to be able to cancel a booking just 2 hours before departure time.
I don't know where can I write this restriction. Should I write it in the model or in the controller application?
This is the pseudo-code I wrote so far:
class CancelValidator < ActiveMOdel::Validator
def validate(record)
if record.date_trip.to_time < Date.now + 2
record.errors[:base] << 'error'
end
end
end
EDIT: This is all the code, but it still lets me destroy the booking.. why?
class CountValidator < ActiveModel::Validator
def validate(record)
if (record.second || record.first)
record.errors[:base]<< ' error '
end
end
end
class DepartureValidator < ActiveModel::Validator
def validate(record)
if record.date_trip.to_date < Date.today
record.errors[:base]<< ' error '
end
end
end
class Reservation < ActiveRecord::Base
validates_with DepartureValidator
validates_with CountValidator
before_destroy :ensure_deletable
belongs_to :dep ,:class_name => 'Stop', :foreign_key => 'dep_id'
belongs_to :arr ,:class_name => 'Stop',:foreign_key => 'arr_id'
belongs_to :route
belongs_to :user
delegate :CountStop, :to => :route, prefix: true, :allow_nil => false
delegate :city ,:to => :arr, :allow_nil => false
delegate :city ,:to => :dep, :allow_nil => false
def division
return Reservation.select{|r| r.route_id == route_id && r.date_trip == date_trip }
end
def second
if (class_point == 2)
y=division.select{ |l| l.class_point == 2 }.count
if(y+1 > route.train.second_class_seats)
return true
end
end
return false
end
def first
if (class_point == 1)
y=division.select{ |l| l.class_point == 1 }.count
if(y+1 > route.train.prima_classe_seats)
return true
end
end
return false
end
def ensure_deletable
self.date_trip.to_time < Time.now + 2
end
end
Since you delete the value, you're going to want to add a callback instead.
The benefit of this is that, before you go and delete the entity, you can decide to stop it outright if it fails your condition.
Here's an example below. Caution: this is untested, but this should give you the gist of things.
class Booking < ActiveRecord::Base
before_destroy :ensure_deletable
private
def ensure_deletable
self.date_trip.to_time < Date.now + 2
end
end
Remember, from the documentation:
The method reference callbacks work by specifying a protected or private method available in the object...

Rails Conditional Validation in Model from Controller Session Data

I am trying to conditionally validate full_name and zip based on whether a visitor is part of a test (Visitors that are part of the test will have certain session data). I am able to pass true/false from the leads controller to the customer model via customer.visitor_test(), but I can't access #test from in_test? in the model. What am I missing?
customer.rb
/* Stripped down code */
class Customer < ActiveRecord::Base
attr_accessor :test
validates :full_name, presence: true, if: :not_in_test?
validates :zip, presence: true, if: :in_test?
def visitor_test(bool)
#test = bool
end
def in_test?
#test
end
def not_in_test?
!self.in_test?
end
end
leads_controller.rb
/* Stripped down code */
class LeadsController < ApplicationController
def create
session[:zip] = zip
session[:email] = email
session[:full_name] = full_name
session[:email_opt_in] = email_opt_in
session[:phone] = phone
listing = Listing.where(id: listing_id).first
customer = create_or_update_customer_from_session(listing)
customer.visitor_test(/* true || false */)
if customer.errors.blank?
/* Do something */
else
/* Something else */
end
end
end
/* Stripped down code */
class Customer < ActiveRecord::Base
attr_accessor :test
validates :full_name, presence: true, if: :not_in_test?
validates :zip, presence: true, if: :in_test?
def in_test?
test
end
def not_in_test?
!in_test?
end
end
attr_accessor provides setter and getter.
/* Stripped down code */
class LeadsController < ApplicationController
def create
session[:zip] = zip
session[:email] = email
session[:full_name] = full_name
session[:email_opt_in] = email_opt_in
session[:phone] = phone
listing = Listing.where(id: listing_id).first
customer = create_or_update_customer_from_session(listing) customer.test = true
customer.save
if customer.errors.blank?
/* Do something */
else
/* Something else */
end
end
end

NameError: uninitialized constant Shipment with Rspec

I'm trying to write rspec test cases with rspec-mock. But, I got an error as follow. order_decorator_spec.rb:149 is let!(:order) { create(:order, shipments: [shipment]) }. It doesn't show any stacktrace perhaps bacause it is executed on RubyMine console. How can I fix the problem??
console
NameError: uninitialized constant Shipment
./spec/models/spree/order_decorator_spec.rb:149:in `block (3 levels) in <top (required)>'
order_decorator_spec.rb
require 'spec_helper'
describe Spree::Order do
context '#after_cancel' do
let!(:shipment) { double(:shipment, :cancel! => '')}
before {shipment}
let!(:order) { create(:order, shipments: [shipment]) }
before do
order.stub(:after_cancel)
end
it "should adsfasdfasdfa" do
shipment.should_receive(:cancel!).once
order.after_cancel
end
end
end
order.rb
require 'spree/core/validators/email'
require 'spree/order/checkout'
module Spree
class Order < ActiveRecord::Base
include Checkout
checkout_flow do
go_to_state :address
go_to_state :delivery
go_to_state :payment, if: ->(order) {
order.update_totals
order.payment_required?
}
go_to_state :confirm, if: ->(order) { order.confirmation_required? }
go_to_state :complete
remove_transition from: :delivery, to: :confirm
end
token_resource
attr_reader :coupon_code
if Spree.user_class
belongs_to :user, class_name: Spree.user_class.to_s
belongs_to :created_by, class_name: Spree.user_class.to_s
else
belongs_to :user
belongs_to :created_by
end
belongs_to :bill_address, foreign_key: :bill_address_id, class_name: 'Spree::Address'
alias_attribute :billing_address, :bill_address
belongs_to :ship_address, foreign_key: :ship_address_id, class_name: 'Spree::Address'
alias_attribute :shipping_address, :ship_address
has_many :state_changes, as: :stateful
has_many :line_items, -> { order('created_at ASC') }, dependent: :destroy
has_many :payments, dependent: :destroy
has_many :return_authorizations, dependent: :destroy
has_many :adjustments, -> { order("#{Adjustment.table_name}.created_at ASC") }, as: :adjustable, dependent: :destroy
has_many :line_item_adjustments, through: :line_items, source: :adjustments
has_many :all_adjustments, class_name: 'Spree::Adjustment'
has_many :inventory_units
has_many :shipments, dependent: :destroy do
def states
pluck(:state).uniq
end
end
accepts_nested_attributes_for :line_items
accepts_nested_attributes_for :bill_address
accepts_nested_attributes_for :ship_address
accepts_nested_attributes_for :payments
accepts_nested_attributes_for :shipments
# Needs to happen before save_permalink is called
before_validation :set_currency
before_validation :generate_order_number, on: :create
before_validation :clone_billing_address, if: :use_billing?
attr_accessor :use_billing
before_create :link_by_email
after_create :create_tax_charge!
validates :email, presence: true, if: :require_email
validates :email, email: true, if: :require_email, allow_blank: true
validate :has_available_shipment
validate :has_available_payment
make_permalink field: :number
class_attribute :update_hooks
self.update_hooks = Set.new
def self.by_number(number)
where(number: number)
end
def self.between(start_date, end_date)
where(created_at: start_date..end_date)
end
def self.by_customer(customer)
joins(:user).where("#{Spree.user_class.table_name}.email" => customer)
end
def self.by_state(state)
where(state: state)
end
def self.complete
where.not(completed_at: nil)
end
def self.incomplete
where(completed_at: nil)
end
# Use this method in other gems that wish to register their own custom logic
# that should be called after Order#update
def self.register_update_hook(hook)
self.update_hooks.add(hook)
end
# For compatiblity with Calculator::PriceSack
def amount
line_items.inject(0.0) { |sum, li| sum + li.amount }
end
def currency
self[:currency] || Spree::Config[:currency]
end
def display_outstanding_balance
Spree::Money.new(outstanding_balance, { currency: currency })
end
def display_item_total
Spree::Money.new(item_total, { currency: currency })
end
def display_adjustment_total
Spree::Money.new(adjustment_total, { currency: currency })
end
def display_tax_total
Spree::Money.new(tax_total, { currency: currency })
end
def display_ship_total
Spree::Money.new(ship_total, { currency: currency })
end
def display_total
Spree::Money.new(total, { currency: currency })
end
def to_param
number.to_s.to_url.upcase
end
def completed?
completed_at.present? || complete?
end
# Indicates whether or not the user is allowed to proceed to checkout.
# Currently this is implemented as a check for whether or not there is at
# least one LineItem in the Order. Feel free to override this logic in your
# own application if you require additional steps before allowing a checkout.
def checkout_allowed?
line_items.count > 0
end
# Is this a free order in which case the payment step should be skipped
def payment_required?
total.to_f > 0.0
end
# If true, causes the confirmation step to happen during the checkout process
def confirmation_required?
Spree::Config[:always_include_confirm_step] ||
payments.valid.map(&:payment_method).compact.any?(&:payment_profiles_supported?) ||
# Little hacky fix for #4117
# If this wasn't here, order would transition to address state on confirm failure
# because there would be no valid payments any more.
state == 'confirm'
end
# Indicates the number of items in the order
def item_count
line_items.inject(0) { |sum, li| sum + li.quantity }
end
def backordered?
shipments.any?(&:backordered?)
end
# Returns the relevant zone (if any) to be used for taxation purposes.
# Uses default tax zone unless there is a specific match
def tax_zone
Zone.match(tax_address) || Zone.default_tax
end
# Indicates whether tax should be backed out of the price calcualtions in
# cases where prices include tax but the customer is not required to pay
# taxes in that case.
def exclude_tax?
return false unless Spree::Config[:prices_inc_tax]
return tax_zone != Zone.default_tax
end
# Returns the address for taxation based on configuration
def tax_address
Spree::Config[:tax_using_ship_address] ? ship_address : bill_address
end
# Array of totals grouped by Adjustment#label. Useful for displaying line item
# adjustments on an invoice. For example, you can display tax breakout for
# cases where tax is included in price.
def line_item_adjustment_totals
Hash[self.line_item_adjustments.eligible.group_by(&:label).map do |label, adjustments|
total = adjustments.sum(&:amount)
[label, Spree::Money.new(total, { currency: currency })]
end]
end
def updater
#updater ||= OrderUpdater.new(self)
end
def update!
updater.update
end
def update_totals
updater.update_totals
end
def clone_billing_address
if bill_address and self.ship_address.nil?
self.ship_address = bill_address.clone
else
self.ship_address.attributes = bill_address.attributes.except('id', 'updated_at', 'created_at')
end
true
end
def allow_cancel?
return false unless completed? and state != 'canceled'
shipment_state.nil? || %w{ready backorder pending}.include?(shipment_state)
end
def awaiting_returns?
return_authorizations.any? { |return_authorization| return_authorization.authorized? }
end
def contents
#contents ||= Spree::OrderContents.new(self)
end
# Associates the specified user with the order.
def associate_user!(user)
self.user = user
self.email = user.email
self.created_by = user if self.created_by.blank?
if persisted?
# immediately persist the changes we just made, but don't use save since we might have an invalid address associated
self.class.unscoped.where(id: id).update_all(email: user.email, user_id: user.id, created_by_id: self.created_by_id)
end
end
# FIXME refactor this method and implement validation using validates_* utilities
def generate_order_number
record = true
while record
random = "R#{Array.new(9){rand(9)}.join}"
record = self.class.where(number: random).first
end
self.number = random if self.number.blank?
self.number
end
def shipped_shipments
shipments.shipped
end
def contains?(variant)
find_line_item_by_variant(variant).present?
end
def quantity_of(variant)
line_item = find_line_item_by_variant(variant)
line_item ? line_item.quantity : 0
end
def find_line_item_by_variant(variant)
line_items.detect { |line_item| line_item.variant_id == variant.id }
end
def ship_total
adjustments.shipping.sum(:amount)
end
# Creates new tax charges if there are any applicable rates. If prices already
# include taxes then price adjustments are created instead.
def create_tax_charge!
Spree::TaxRate.adjust(self)
end
def outstanding_balance
total - payment_total
end
def outstanding_balance?
self.outstanding_balance != 0
end
def name
if (address = bill_address || ship_address)
"#{address.firstname} #{address.lastname}"
end
end
def can_ship?
self.complete? || self.resumed? || self.awaiting_return? || self.returned?
end
def credit_cards
credit_card_ids = payments.from_credit_card.pluck(:source_id).uniq
CreditCard.where(id: credit_card_ids)
end
# Finalizes an in progress order after checkout is complete.
# Called after transition to complete state when payments will have been processed
def finalize!
touch :completed_at
# lock all adjustments (coupon promotions, etc.)
adjustments.update_all state: 'closed'
# update payment and shipment(s) states, and save
updater.update_payment_state
shipments.each do |shipment|
shipment.update!(self)
shipment.finalize!
end
updater.update_shipment_state
save
updater.run_hooks
deliver_order_confirmation_email
end
def deliver_order_confirmation_email
begin
OrderMailer.confirm_email(self.id).deliver
rescue Exception => e
logger.error("#{e.class.name}: #{e.message}")
logger.error(e.backtrace * "\n")
end
end
# Helper methods for checkout steps
def paid?
payment_state == 'paid' || payment_state == 'credit_owed'
end
def available_payment_methods
#available_payment_methods ||= (PaymentMethod.available(:front_end) + PaymentMethod.available(:both)).uniq
end
def pending_payments
payments.select(&:checkout?)
end
# processes any pending payments and must return a boolean as it's
# return value is used by the checkout state_machine to determine
# success or failure of the 'complete' event for the order
#
# Returns:
# - true if all pending_payments processed successfully
# - true if a payment failed, ie. raised a GatewayError
# which gets rescued and converted to TRUE when
# :allow_checkout_gateway_error is set to true
# - false if a payment failed, ie. raised a GatewayError
# which gets rescued and converted to FALSE when
# :allow_checkout_on_gateway_error is set to false
#
def process_payments!
if pending_payments.empty?
raise Core::GatewayError.new Spree.t(:no_pending_payments)
else
pending_payments.each do |payment|
break if payment_total >= total
payment.process!
if payment.completed?
self.payment_total += payment.amount
end
end
end
rescue Core::GatewayError => e
result = !!Spree::Config[:allow_checkout_on_gateway_error]
errors.add(:base, e.message) and return result
end
def billing_firstname
bill_address.try(:firstname)
end
def billing_lastname
bill_address.try(:lastname)
end
def products
line_items.map(&:product)
end
def variants
line_items.map(&:variant)
end
def insufficient_stock_lines
line_items.select &:insufficient_stock?
end
def merge!(order, user = nil)
order.line_items.each do |line_item|
next unless line_item.currency == currency
current_line_item = self.line_items.find_by(variant: line_item.variant)
if current_line_item
current_line_item.quantity += line_item.quantity
current_line_item.save
else
line_item.order_id = self.id
line_item.save
end
end
self.associate_user!(user) if !self.user && !user.blank?
# So that the destroy doesn't take out line items which may have been re-assigned
order.line_items.reload
order.destroy
end
(snip)
(snip)
(snip)
(snip)
(snip)
(snip)
(snip)
(snip)
end
end
shipment_decorator.rb
require 'ostruct'
module Spree
Shipment.class_eval do
def after_ship
inventory_units.each &:ship!
adjustment.finalize!
#send_shipped_email
touch :shipped_at
end
end
end
shipment.rb
require 'ostruct'
module Spree
class Shipment < ActiveRecord::Base
belongs_to :order, class_name: 'Spree::Order', touch: true
belongs_to :address, class_name: 'Spree::Address'
belongs_to :stock_location, class_name: 'Spree::StockLocation'
has_many :shipping_rates, dependent: :delete_all
has_many :shipping_methods, through: :shipping_rates
has_many :state_changes, as: :stateful
has_many :inventory_units, dependent: :delete_all
has_one :adjustment, as: :source, dependent: :destroy
after_save :ensure_correct_adjustment, :update_order
attr_accessor :special_instructions
accepts_nested_attributes_for :address
accepts_nested_attributes_for :inventory_units
make_permalink field: :number, length: 11, prefix: 'H'
scope :shipped, -> { with_state('shipped') }
scope :ready, -> { with_state('ready') }
scope :pending, -> { with_state('pending') }
scope :with_state, ->(*s) { where(state: s) }
scope :trackable, -> { where("tracking IS NOT NULL AND tracking != ''") }
# shipment state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
state_machine initial: :pending, use_transactions: false do
event :ready do
transition from: :pending, to: :ready, if: lambda { |shipment|
# Fix for #2040
shipment.determine_state(shipment.order) == 'ready'
}
end
event :pend do
transition from: :ready, to: :pending
end
event :ship do
transition from: :ready, to: :shipped
end
after_transition to: :shipped, do: :after_ship
event :cancel do
transition to: :canceled, from: [:pending, :ready]
end
after_transition to: :canceled, do: :after_cancel
event :resume do
transition from: :canceled, to: :ready, if: lambda { |shipment|
shipment.determine_state(shipment.order) == :ready
}
transition from: :canceled, to: :pending, if: lambda { |shipment|
shipment.determine_state(shipment.order) == :ready
}
transition from: :canceled, to: :pending
end
after_transition from: :canceled, to: [:pending, :ready], do: :after_resume
end
def to_param
number
end
def backordered?
inventory_units.any? { |inventory_unit| inventory_unit.backordered? }
end
def shipped=(value)
return unless value == '1' && shipped_at.nil?
self.shipped_at = Time.now
end
def shipping_method
selected_shipping_rate.try(:shipping_method) || shipping_rates.first.try(:shipping_method)
end
def add_shipping_method(shipping_method, selected = false)
shipping_rates.create(shipping_method: shipping_method, selected: selected)
end
def selected_shipping_rate
shipping_rates.where(selected: true).first
end
def selected_shipping_rate_id
selected_shipping_rate.try(:id)
end
def selected_shipping_rate_id=(id)
shipping_rates.update_all(selected: false)
shipping_rates.update(id, selected: true)
self.save!
end
def refresh_rates
return shipping_rates if shipped?
return [] unless can_get_rates?
# StockEstimator.new assigment below will replace the current shipping_method
original_shipping_method_id = shipping_method.try(:id)
self.shipping_rates = Stock::Estimator.new(order).shipping_rates(to_package)
if shipping_method
selected_rate = shipping_rates.detect { |rate|
rate.shipping_method_id == original_shipping_method_id
}
self.selected_shipping_rate_id = selected_rate.id if selected_rate
end
shipping_rates
end
def currency
order ? order.currency : Spree::Config[:currency]
end
# The adjustment amount associated with this shipment (if any.) Returns only the first adjustment to match
# the shipment but there should never really be more than one.
def cost
adjustment ? adjustment.amount : 0
end
alias_method :amount, :cost
def display_cost
Spree::Money.new(cost, { currency: currency })
end
alias_method :display_amount, :display_cost
def item_cost
line_items.map(&:amount).sum
end
def display_item_cost
Spree::Money.new(item_cost, { currency: currency })
end
def total_cost
cost + item_cost
end
def display_total_cost
Spree::Money.new(total_cost, { currency: currency })
end
def editable_by?(user)
!shipped?
end
def manifest
inventory_units.group_by(&:variant).map do |variant, units|
states = {}
units.group_by(&:state).each { |state, iu| states[state] = iu.count }
OpenStruct.new(variant: variant, quantity: units.length, states: states)
end
end
def line_items
if order.complete? and Spree::Config.track_inventory_levels
order.line_items.select { |li| !li.should_track_inventory? || inventory_units.pluck(:variant_id).include?(li.variant_id) }
else
order.line_items
end
end
def finalize!
InventoryUnit.finalize_units!(inventory_units)
manifest.each { |item| manifest_unstock(item) }
end
def after_cancel
manifest.each { |item| manifest_restock(item) }
end
def after_resume
manifest.each { |item| manifest_unstock(item) }
end
# Updates various aspects of the Shipment while bypassing any callbacks. Note that this method takes an explicit reference to the
# Order object. This is necessary because the association actually has a stale (and unsaved) copy of the Order and so it will not
# yield the correct results.
def update!(order)
old_state = state
new_state = determine_state(order)
update_column :state, new_state
after_ship if new_state == 'shipped' and old_state != 'shipped'
end
# Determines the appropriate +state+ according to the following logic:
#
# pending unless order is complete and +order.payment_state+ is +paid+
# shipped if already shipped (ie. does not change the state)
# ready all other cases
def determine_state(order)
return 'canceled' if order.canceled?
return 'pending' unless order.can_ship?
return 'pending' if inventory_units.any? &:backordered?
return 'shipped' if state == 'shipped'
order.paid? ? 'ready' : 'pending'
end
def tracking_url
#tracking_url ||= shipping_method.build_tracking_url(tracking)
end
def include?(variant)
inventory_units_for(variant).present?
end
def inventory_units_for(variant)
inventory_units.group_by(&:variant_id)[variant.id] || []
end
def to_package
package = Stock::Package.new(stock_location, order)
inventory_units.includes(:variant).each do |inventory_unit|
package.add inventory_unit.variant, 1, inventory_unit.state_name
end
package
end
def set_up_inventory(state, variant, order)
self.inventory_units.create(variant_id: variant.id, state: state, order_id: order.id)
end
private
def manifest_unstock(item)
stock_location.unstock item.variant, item.quantity, self
end
def manifest_restock(item)
if item.states["on_hand"].to_i > 0
stock_location.restock item.variant, item.states["on_hand"], self
end
if item.states["backordered"].to_i > 0
stock_location.restock_backordered item.variant, item.states["backordered"]
end
end
def description_for_shipping_charge
"#{Spree.t(:shipping)} (#{shipping_method.name})"
end
def after_ship
inventory_units.each &:ship!
adjustment.finalize!
send_shipped_email
touch :shipped_at
end
def send_shipped_email
ShipmentMailer.shipped_email(self.id).deliver
end
def ensure_correct_adjustment
if adjustment
adjustment.originator = shipping_method
adjustment.label = shipping_method.adjustment_label
adjustment.amount = selected_shipping_rate.cost if adjustment.open?
adjustment.save!
adjustment.reload
elsif selected_shipping_rate_id
shipping_method.create_adjustment shipping_method.adjustment_label, order, self, true, "open"
reload #ensure adjustment is present on later saves
end
end
def update_order
order.update!
end
def can_get_rates?
order.ship_address && order.ship_address.valid?
end
end
end

Resources