ruby-on-railsbefore-save

What is the rails way to update a price field before update in a model object generating a total?


I have an order form with selectors for a product and boolean sliders for add-on options. I want to calculate the total price before saving to the db. I am using a before_save :set_total action in the model, but I can't change the value of the price field. Are model values read only? If so, where should I put the model object 'set_total' to affect the price value? I don't think it is wise to use an after-save :set_total with an update olineorders set price = #{tot price};

my model file extract:

class Olineorder < ApplicationRecord

    before_save :set_total
    
    (other stuff in here)
    
private

    def set_total

        puts "\n\ndef set_total"
        puts "\tprice: #{price.inspect}\t#{Olineorder.inspect}\t\n"
        
            @a = Price.find_by_sql("select pr.product_id, pr.price as theprice, p.name from prices as pr, products as p where pr.product_id == p.id;")
            @b = Hash.new
            @a.each do |f|
                @b[f.name] = f.theprice.to_f
#               puts f.name, f.theprice
            end

        totprice = @b["#{selection}"]
        totprice += @b["Certificate"] if cert == true
        totprice += @b["Biographical Skectch"] if bio == true
        totprice += @b["Photograph"] if pic == true
        self.price = printf('$%.2f',totprice)
==>Note:  I al so tried:  price = ....  with same result. <===

        puts "\n\tprice: #{price.inspect}  -- total price -- #{totprice}\t#{Olineorder.inspect}\t\n"
        puts "\ndef_set_toal ---<<< END\n"
        
    end

Below is the logging output in the development.log

------------------ Output in log file ---------------
Started POST "/olineorders" for ::1 at 2024-10-14 18:04:10 -0700
Processing by OlineordersController#create as HTML
  Parameters: {"authenticity_token"=>"[FILTERED]", "olineorder"=>{--REDACTED---, "selection"=>"Worker's Brick", "cert"=>"true", "bio"=>"true", "pic"=>"true", "brick1"=>"", "brick2"=>"", "brick3"=>"", "price"=>"0.00", "comments"=>"", "block"=>"", "log"=>"0", "status"=>""}, "commit"=>"Create Olineorder"}
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."remember_token" = ? ORDER BY "users"."id" ASC LIMIT ?  [["remember_token", "[FILTERED]"], ["LIMIT", 1]]
  ↳ app/controllers/application_controller.rb:14:in `ck_for_accs_level'
  Product Load (0.1ms)  select id, name from products;
  ↳ app/controllers/olineorders_controller.rb:113:in `set_product_list'


def set_total
    price: "0.00"   Olineorder(id: integer, last_name: string, first_name: string, address1: string, address2: string, city: string, state: string, zip: string, phone: string, email: string, selection: string, cert: boolean, bio: boolean, pic: boolean, comments: string, block: string, log: string, brick1: string, brick2: string, brick3: string, status: string, price: string, created_at: datetime, updated_at: datetime)
  TRANSACTION (0.0ms)  begin transaction
  ↳ app/models/olineorder.rb:35:in `set_total'
  Price Load (0.4ms)  select pr.product_id, pr.price as theprice, p.name from prices as pr, products as p where pr.product_id == p.id;
  ↳ app/models/olineorder.rb:35:in `set_total'
$135.00
    price: nil  -- total price -- 135.0 Olineorder(id: integer--redacted---, price: string, created_at: datetime, updated_at: datetime)

def_set_toal ---<<< END
  Olineorder Create (0.1ms)  INSERT INTO "olineorders" (---redacted---- "price", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING "id"  [---REDACTED---, ["price", "0.00"], ["created_at", "2024-10-15 01:04:11.142454"], ["updated_at", "2024-10-15 01:04:11.142454"]]
  ↳ app/controllers/olineorders_controller.rb:41:in `block in create'
  TRANSACTION (0.6ms)  commit transaction
  ↳ app/controllers/olineorders_controller.rb:41:in `block in create'
Redirected to http://localhost:3000/olineorders/17
Completed 302 Found in 41ms (ActiveRecord: 2.8ms (4 queries, 0 cached) | GC: 1.0ms)


Started GET "/olineorders/17" for ::1 at 2024-10-14 18:04:11 -0700
Processing by OlineordersController#show as HTML
  Parameters: {"id"=>"17"}
  Olineorder Load (0.1ms)  SELECT "olineorders".* FROM "olineorders" WHERE "olineorders"."id" = ? LIMIT ?  [["id", 17], ["LIMIT", 1]]
  ↳ app/controllers/olineorders_controller.rb:109:in `set_olineorder'
  User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."remember_token" = ? ORDER BY "users"."id" ASC LIMIT ?  [["remember_token", "[FILTERED]"], ["LIMIT", 1]]
  ↳ app/controllers/application_controller.rb:14:in `ck_for_accs_level'
  Product Load (0.0ms)  select id, name from products;
  ↳ app/controllers/olineorders_controller.rb:113:in `set_product_list'
  Rendering layout layouts/application.html.erb
  Rendering olineorders/show.html.erb within layouts/application
  Rendered olineorders/_olineorder.html.erb (Duration: 0.3ms | GC: 0.0ms)
  Rendered olineorders/show.html.erb within layouts/application (Duration: 1.7ms | GC: 0.0ms)
  Rendered layouts/_pg_links.html.erb (Duration: 0.4ms | GC: 0.0ms)
  Rendered layout layouts/application.html.erb (Duration: 3.3ms | GC: 0.1ms)
Completed 200 OK in 9ms (Views: 5.0ms | ActiveRecord: 0.2ms (3 queries, 0 cached) | GC: 0.2ms)

Solution

  • self.price = printf('$%.2f',totprice)
    

    This does not set the price, it prints it. printf is for printing and returns nil so that does self.price = nil. You want sprintf.

    If price is a number you probably don't want to add a $ to it. Generally avoid adding formatting to numbers in your database, it makes them harder to work with. In that case, use Numeric#round.

    self.price = totprice.round(2)
    

    Notes

    Consider using rubocop and rubocop-rails to guide you.