Search code examples
ruby-on-railsrubymoney-rails

Running into PG::NotNullViolation


Ok, so I integrated money-rails gem into my basic calculating app ...

When I try to run it now, I get this error:

ActiveRecord::StatementInvalid in TippiesController#create

and this line underneath it:

PG::NotNullViolation: ERROR: null value in column "tip" violates not-null constraint DETAIL: Failing row contains (37, null, null, 2017-03-10 07:47:36.152504, 2017-03-10 07:47:36.152504, 3300, USD, 10, USD). : INSERT INTO "tippies" ("created_at", "updated_at", "cost_cents", "tip_cents") VALUES ($1, $2, $3, $4) RETURNING "id"" ...

So, I thought this meant that it was generating this because my my model stipulates validates :tip, presence: true ... so I commented it out, but still receive this error.

Not sure how to sort it out. Any help in figuring it out would be appreciated.

Current Model

class Tippy < ApplicationRecord

    validates :tip_cents, presence: true
    validates :cost_cents, presence: true

    monetize :tip_cents
    monetize :cost_cents


    TIP_CHOICES = { "10%" => ".10", "20%" => ".20", "30%" => ".30", "40%" => ".40", "50%" => ".50", 
                    "60%" => ".60", "70%" => ".70", "80%" => ".80", "90%" => ".90" }



    def calculation_of_total_cost 
        cost_cents + (tip_cents * cost_cents)
    end

end

Current Controller

class TippiesController < ApplicationController
  before_action :set_tippy, only: [:show, :edit, :update, :destroy]

  # GET /tippies
  # GET /tippies.json
  def index
    @tippies = Tippy.all
  end

  # GET /tippies/1
  # GET /tippies/1.json
  def show
    #@calculation_of_total_cost 
  end

  # GET /tippies/new
  def new
    @tippy = Tippy.new
  end

  # GET /tippies/1/edit
  def edit
  end

  # POST /tippies
  # POST /tippies.json
  def create
    @tippy = Tippy.new(tippy_params)

    respond_to do |format|
      if @tippy.save
        format.html { redirect_to @tippy, notice: 'Tippy was successfully created.' }
        format.json { render :show, status: :created, location: @tippy }
      else
        format.html { render :new }
        format.json { render json: @tippy.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /tippies/1
  # PATCH/PUT /tippies/1.json
  def update
    respond_to do |format|
      if @tippy.update(tippy_params)
        format.html { redirect_to @tippy, notice: 'Tippy was successfully updated.' }
        format.json { render :show, status: :ok, location: @tippy }
      else
        format.html { render :edit }
        format.json { render json: @tippy.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /tippies/1
  # DELETE /tippies/1.json
  def destroy
    @tippy.destroy
    respond_to do |format|
      format.html { redirect_to tippies_url, notice: 'Tippy was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_tippy
      @tippy = Tippy.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def tippy_params
      params.require(:tippy).permit(:tip, :cost, :tip_cents, :tip_currency, :cost_cents, :cost_currency)
    end
end

Migrated Monetize File

class MonetizeTippy < ActiveRecord::Migration[5.0]
  def change
    add_monetize :tippies, :cost
    add_monetize :tippies, :tip
  end
end

Schema

ActiveRecord::Schema.define(version: 20170310070749) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "tippies", force: :cascade do |t|
    t.float    "tip",                           null: false
    t.decimal  "cost",                          null: false
    t.datetime "created_at",                    null: false
    t.datetime "updated_at",                    null: false
    t.integer  "cost_cents",    default: 0,     null: false
    t.string   "cost_currency", default: "USD", null: false
    t.integer  "tip_cents",     default: 0,     null: false
    t.string   "tip_currency",  default: "USD", null: false
  end

end

Show.html.erb (this page pops up after the home page which is new action

<br/><br/>
<h1 class="text-center">Your Total Cost</h1>
<br/><br />


<table class="table table-striped">
    <tr>
        <td>
            Cost of Your Meal:
        </td>
        <td>
            <%= humanized_money_with_symbol @tippy.cost_cents %>
        </td>
    </tr>
    <tr>
        <td>
            Tip You Picked:
        </td>
        <td>
             <%= number_to_percentage(@tippy.tip_cents * 100, format: "%n%", precision: 0) %>
        </td>
    </tr>
    <tr>
        <td>
            The Total Cost:
        </td>
        <td>
            <%= humanized_money_with_symbol @tippy.calculation_of_total_cost %>
        </td>
    </tr>
</table>

new.html.erb

<br /><br />
<h1 class="text-center">Calculate Your Tip!</h1>

<%= render 'form', tippy: @tippy %>

_form.html.erb

<%= form_for(tippy, :html => {'class' => "form-horizontal"}) do |f| %>
  <% if tippy.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(tippy.errors.count, "error") %> prohibited this tippy from being saved:</h2>

      <ul>
      <% tippy.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>


  <div class="field form-group">
    <%= f.label :cost_of_your_meal, class: "control-label" %>
    <%= f.text_field :cost, class: "form-control" %>
  </div>

  <div class="field form-group">
    <%= f.label :pick_your_tip, class: "control-label" %>
    <%= f.select(:tip, Tippy::TIP_CHOICES, class: "form-control")  %>
  </div>


  <div class="actions">
    <%= f.submit class: "btn btn-primary btn-lg btn-block" %>
  </div>
<% end %>

Solution

  • This error comes from the database, not Rails.

    In the migration where the tip column gets added, it will look something like this:

    t.float :tip, null: false
    

    This translates to a NOT NULL constraint in your SQL schema.

    A database constraint and a validation do not serve the same purpose: the former is for definitely preventing incorrect data from being persisted, whereas the latter is an easy way to communicate errors back to your users through the automatically added ActiveModel::Errors object.

    To solve your problem you have to add another migration and make tip nullable again (and maybe provide an appropriate default value):

    change_column :tippies, :tip, :float, null: true, default: 0