Incrementing repeated items in cart rails 4 - ruby-on-rails

I am having a problem when I add items into my cart where instead of updating an item if it already exists, it creates a duplicate item. I know I need to pass a validation to check if the order_item exists but I'm not quite sure how or where to do it.
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
helper_method :current_order
helper_method :current_buylist_order
def current_order
if !session[:order_id].nil?
Order.find(session[:order_id])
else
Order.new
end
end
end
class OrderItemsController < ApplicationController
def create
#order = current_order
#order_item = #order.order_items.new(order_item_params)
#order.save
session[:order_id] = #order.id
end
def update
#order = current_order
#order_item = #order.order_items.find(params[:id])
#order_item.update_attributes(order_item_params)
#order_items = #order.order_items
end
def destroy
#order = current_order
#order_item = #order.order_items.find(params[:id])
#order_item.destroy
#order_items = #order.order_items
end
private
def order_item_params
params.require(:order_item).permit(:quantity, :card_id)
end
end
class CartsController < ApplicationController
def show
#order_items = current_order.order_items
end
end
class Order < ActiveRecord::Base
belongs_to :order_status
has_many :order_items
before_create :set_order_status
before_save :update_subtotal
def subtotal
order_items.collect { |oi| oi.valid? ? (oi.quantity * oi.unit_price) : 0 }.sum
end
private
def set_order_status
self.order_status_id = 1
end
def update_subtotal
self[:subtotal] = subtotal
end
end
class OrderItem < ActiveRecord::Base
belongs_to :card
belongs_to :order
validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
validate :card_present
validate :order_present
before_save :finalize
def unit_price
if persisted?
self[:unit_price]
else
card.price
end
end
def total_price
unit_price * quantity
end
private
def card_present
if card.nil?
errors.add(:card, "is not valid or is not active.")
end
end
def order_present
if order.nil?
errors.add(:order, "is not a valid order.")
end
end
def finalize
self[:unit_price] = unit_price
self[:total_price] = quantity * self[:unit_price]
end
end

In you OrderItemsController, in create and update methods you are using line #order_item = #order.order_items.new(order_item_params) which initializes new item every time one of these methods is called. You need to change these methods, so they create new item only if item with same params doesn't already exist. Take a look at first_or_initialize or first_or_create method. Or use a simple condition (Eg if item exists in the order increase quantity by one, else create new item)

In your Order Items Controller, under the create method, have a code that checks for an existing product then increments the quantity if you add to cart more than once. You can use jQuery to disable the button too.
Under the create method in order_items_controller
#product_id = params['order_item']['product_id']
#order.order_items.each do |oi|
if oi.product_id.to_s == #product_id
updater(oi.id,params['order_item']['quantity']) and return
end
end
Add and extra method for the update in quantity
def updater(id,quantity)
#order = current_order
#order_item = #order.order_items.find(id)
quantity = #order_item.quantity + quantity.to_i
#order_item.update(quantity:quantity)
#order_items = #order.order_items
end

What worked for me is this:
def create
#order = current_order
if OrderItem.where("card_id = ? AND order_id = ?",
params[:order_item][:card_id], #order.id.to_s).empty?
#order_item = OrderItem.new(order_item_params)
#order_item.order = current_order
#order_item.save
else
#order_item = OrderItem.where("card_id = ? AND order_id = ?",
params[:order_item][:card_id], #order.id.to_s).first
quantity = #order_item.quantity += params[:order_item][:quantity].to_i
#order_item.update(quantity: quantity)
end
end

Related

Check if record exists inside create action

My scenario:
I have SaleItem model, which stores (:sale_id, :product_id, :quantity, :total_price).
class SaleItem < ApplicationRecord
belongs_to :sale
belongs_to :product
before_save :set_unit_price
before_save :set_total_price
before_save :set_total_disscounted_price
def unit_price
if persisted?
self[:unit_price]
else
product.price_pence
end
end
def total_price
unit_price * quantity
end
private
def set_unit_price
self[:unit_price] = unit_price
end
def set_total_price
self[:total_price] = quantity * set_unit_price
end
def set_total_disscounted_price
self[:total_disscounted_price] =self[:total_price] - (self[:total_price] * self.sale.event.event_disscount) / 100
end
end
My problem is, whenever I create new SaleItem object I want to check if the same record already exists and if it does then I just need to add up the :quantity and recalculate total price (in the model) according to the new :quantity.
What I am confused about is is it possible to check it within the create method?
So far I found this rails method first_or_create.
This is my initial code from SaleItem controller
class SaleItemsController < ApplicationController
def create
#sale = #sale_item.sale
#sale_item = SaleItem.create(sale_item_params)
#event = #sale.event
if #sale_item.save
redirect_to event_sale_path(#event, #sale)
else
redirect_to event_sale_path(#event, #sale)
end
end
private
def sale_item_params
params.require(:sale_item).permit(:sale_id, :product_id, :quantity)
end
end
and then after I found first_or_create I started changing it to this, but havent finished as I am a bit stuck:
class SaleItemsController < ApplicationController
def create
#sale_item = SaleItem.where(sale_id: params[:sale_id], product_id: sale_item_params[:product_id]).first_or_create do |sale_item|
sale_item.quantity = sale_item.quantity.to_i + sale_item_params[:quantity].to_i
end
#sale = #sale_item.sale
#sale_item = SaleItem.create(sale_item_params)
#event = #sale.event
if #sale_item.save
redirect_to event_sale_path(#event, #sale)
else
redirect_to event_sale_path(#event, #sale)
end
end
private
def sale_item_params
params.require(:sale_item).permit(:sale_id, :product_id, :quantity)
end
end
I couldn't figure out how to update record that was found and dont change newly created record, so I created a variable #new_record and set it initially to false, and then once new record created, #new_record changes to true. In this way I was being able to track each record and change it if needed.
This is the SaleItem Controller updated code
class SaleItemsController < ApplicationController
def create
#new_record = false
#sale_item = SaleItem.where(sale_id: params[:sale_id], product_id: sale_item_params[:product_id]).first_or_create do |sale_item|
sale_item.quantity = sale_item_params[:quantity]
#new_record = true
end
#new_record == false ? #sale_item.quantity = #sale_item.quantity.to_i + sale_item_params[:quantity].to_i : sale_item_params[:quantity].to_i
#sale = #sale_item.sale
#event = #sale.event
if #sale_item.save
redirect_to event_sale_path(#event, #sale)
else
redirect_to event_sale_path(#event, #sale)
end
end
def destroy
#sale_item = SaleItem.find(params[:id])
#sale = #sale_item.sale
#event = #sale.event
#sale_item.destroy
redirect_to event_sale_path(#event, #sale)
end
private
def sale_item_params
params.require(:sale_item).permit(:sale_id, :product_id, :quantity)
end
end
Change your create action code from this
def create
#sale_item = SaleItem.where(sale_id: params[:sale_id], product_id: sale_item_params[:product_id]).first_or_create do |sale_item|
sale_item.quantity = sale_item.quantity.to_i + sale_item_params[:quantity].to_i
end
#sale = #sale_item.sale
#sale_item = SaleItem.create(sale_item_params)
#event = #sale.event
if #sale_item.save
redirect_to event_sale_path(#event, #sale)
else
redirect_to event_sale_path(#event, #sale)
end
end
to this
def create
#sale_item = SaleItem.find_or_create_by(sale_item_params) do |sale_item|
sale_item.quantity = sale_item.quantity.to_i + sale_item_params[:quantity].to_i
sale_item.save
end
#sale = #sale_item.sale
#event = #sale.event
if #sale_item.save
redirect_to event_sale_path(#event, #sale)
else
redirect_to event_sale_path(#event, #sale)
end
end
find_or_create_by(sale_item_params) will first find the sale_item with sale_id, product_id and quantity and then if there isn't any sale_item record found it will create a new one, In both case you can update the quantity.
I think you have to define the unique key to find SaleItem
for example, if sale_id and product_id are keys to find SaleItem.
it's better to have new strong parameter method for find or create SaleItem on create method.
controller example:
class SaleItemsController < ApplicationController
before_action :initialize_sale_item, only: [:create]
def create
#sale_item.assign_attributes(sale_item_params)
#sale_item.save
# render or respond_to ...
end
private
def sale_item_params
params.require(:sale_item).permit(:sale_id, :product_id, :quantity)
end
def sale_item_key_params
params.require(:sale_item).permit(:sale_id, :product_id)
end
def initialize_sale_item
#sale_item = SaleItem.find_or_initialize_by(sale_item_key_params)
end
end
as Talha Junaid suggested, you can use find_or_create_by instead of using find_or_initialize_by(and remove initialize_sale_item ...)
references
https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-find_or_initialize_by
https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-find_or_create_by
My guess it you probably want to look at https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-find_or_create_by

Updating in stock quantity based on shopping cart quantity in rails app?

I'm working on a E Commerce app and I can´t figure out how I can update the product stock quantity when the user adds product to the cart.
I followed a tutorial for most of this app since I'm a total newbie. But the tutorial doesn't go into how to update the product stock quantity when the user adds product to the cart.
so far I've added this method to the cart.rbmodel
def upd_quantity
if cart.purchased_at
product.decrement!(quantity: params[:stock_quantity])
end
end
But I´m really stuck and don't know what I'm doing
It would be great if someone could take a look at this and advise me how to implement this function to the app.
here is a link to the github repo https://github.com/DadiHall/brainstore
this what I have at the moment.
cart_item.rb model
class CartItem
attr_reader :product_id, :quantity
def initialize product_id, quantity = 1
#product_id = product_id
#quantity = quantity
end
def increment
#quantity = #quantity + 1
end
def product
Product.find product_id
end
def total_price
product.price * quantity
end
def upd_quantity
if cart.purchased_at
product.decrement!(quantity: params[:stock_quantity])
end
end
end
cart.rbmodel
class Cart
attr_reader :items
def self.build_from_hash hash
items = if hash ["cart"] then
hash["cart"] ["items"].map do |item_data|
CartItem.new item_data["product_id"], item_data["quantity"]
end
else
[]
end
new items
end
def initialize items = []
#items = items
end
def add_item product_id
item = #items.find { |item| item.product_id == product_id }
if item
item.increment
else
#items << CartItem.new(product_id)
end
end
def empty?
#items.empty?
end
def count
#items.length
end
def serialize
items = #items.map do |item|
{
"product_id" => item.product_id,
"quantity" => item.quantity
}
end
{
"items" => items
}
end
def total_price
#items.inject(0) { |sum, item| sum + item.total_price }
end
end
product.rb model
class Product < ActiveRecord::Base
mount_uploader :image, ImageUploader
validates_presence_of :name, :price, :stock_quantity
validates_numericality_of :price, :stock_quantity
belongs_to :designer
belongs_to :category
belongs_to :page
def self.search(query)
where("name LIKE ? OR description LIKE ?", "%#{query}%", "%#{query}%")
end
end
`cart_controller.rb`
class CartsController < ApplicationController
before_filter :initialize_cart
def add
#cart.add_item params[:id]
session["cart"] = #cart.serialize
product = Product.find params[:id]
redirect_to :back, notice: "Added #{product.name} to cart."
end
def show
end
def checkout
#order_form = OrderForm.new user: User.new
#client_token = Braintree::ClientToken.generate
end
def remove
cart = session['cart']
item = cart['items'].find { |item| item['product_id'] == params[:id] }
if item
cart['items'].delete item
end
redirect_to cart_path
end
end
Does adding this line:
product.update_columns(stock_quantity: product.stock_quantity - 1)
To your add action in the CartsController do the trick?
def add
#cart.add_item params[:id]
session["cart"] = #cart.serialize
product = Product.find params[:id]
product.update_columns(stock_quantity: product.quantity - 1)
redirect_to :back, notice: "Added #{product.name} to cart."
end
Try this for removing the product from cart:
def remove
cart = session['cart']
item = cart['items'].find { |item| item['product_id'] == params[:id] }
product = Product.find(item['product_id'])
product.update_columns(stock_quantity: product.stock_quantity + 1)
if item
cart['items'].delete item
end
redirect_to cart_path
end
Note, this will only work if you are adding and removing 1 item/product at a time. I suggest renaming 'item' to 'product' in your remove action for consistency.

NoMethodError in EventsController#create undefined method `build' for nil:NilClass?

I'm getting a bit confused as to why I am receiving this error as (i think) it is suggesting that my current_customer returns nil. I do not know how to fix this, is it a problem with my current_customer method, sessions controller, or my events_controller...
Event Model:
class Event < ActiveRecord::Base
belongs_to :calendar
end
Events Controller:
class EventsController < ApplicationController
def new
#event = Event.new
#calendar = current_customer.calendar #WHY IS IT NOT ERRORING HERE TOO?
end
def create
**#calendar = current_customer.calendar #IT IS CALLING ERROR HERE**
#event = #calendar.build.event(event_params)
if #event.save
redirect_to '/main'
else
redirect_to '/compose'
end
end
private
def event_params
params.require(:event).permit(:calendar_id, :name, :starts_at, :ends_at)
end
Customer model:
class Customer < ActiveRecord::Base
belongs_to :business
has_one :calendar
has_secure_password
attr_accessor :remember_token
#remembers a user in the database for use in persistent sessions
def remember
self.remember_token = Customer.new_token
update_attribute(:remember_digest, Customer.digest(remember_token))
end
def Customer.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
def forget
update_attribute(:remember_digest, nil)
end
def Customer.new_token
SecureRandom.urlsafe_base64
end
#returns true if the given token matches the digest
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
end
Customer controller:
class CustomersController < ApplicationController
def new
#customer = Customer.new
#businesses = Business.all
#calendar = Calendar.new
end
def create
#customer = Customer.create(customer_params)
#calendar = #customer.build_calendar
#customer.save!
session[:customer_id] = #customer.id
redirect_to '/'
rescue ActiveRecord::RecordInvalid => ex
render action: 'new', alert: ex.message
end
private
def customer_params
params.require(:customer).permit(:first_name, :last_name, :business_no, :email, :password, :business_id)
end
end
Sessions controller:
class SessionsController < ApplicationController
def new
end
def create
#customer = Customer.find_by_email(params[:session][:email])
if #customer && #customer.authenticate(params[:session][:password])
session[:customer_id] = #customer.id #log in
#customer.remember
cookies.permanent.signed[:customer_id] = #customer.id
cookies.permanent[:remember_token] = #customer.remember_token
redirect_to '/main'
else
#need to add flash error: invalid password/email combination
redirect_to '/login'
end
end
def destroy
#current_customer.forget
cookies.delete(:customer_id)
cookies.delete(:remember_token)
session.delete(:customer_id)
#current_customer = nil
redirect_to '/'
end
end
Current_customer method within application.controller:
helper_method :current_customer
def current_customer
if (customer_id = session[:customer_id])
#current_customer ||= Customer.find_by(id: customer_id)
elsif (customer_id = cookies.signed[:customer_id])
customer = Customer.find_by(id: customer_id)
if customer && customer.authenticated?(cookies[:remember_token])
session[:customer_id] = customer.id #log in
#current_customer = customer
end
end
end
Calendar model:
class Calendar < ActiveRecord::Base
belongs_to :customer
has_many :events
end
calendars_controller:
class CalendarsController < ApplicationController
def new
#calendar = Calendar.new(calendar_params)
end
def create
#calendar = Calendar.new(calendar_params)
end
private
def calendar_params
params.require(:customer_id)
end
end
I am new to Ruby/Rails so I would really appreciate any explanations and answers, please help! thanks
#calendar = current_customer.calendar
#event = #calendar.build.event(event_params)
According to the error message, #calendar is nil, so when you call #calendar.build.event(event_params) in the next line, it calls: nil.build and hence you get the error.
For some reason, your current_customer's calendar is nil for that request. Figure that out and make sure the current_customer has a calendar present in the database, then it should work.
Also, you need to change:
#event = #calendar.build.event(event_params)
To:
#event = #calendar.events.build(event_params)
It would seem that #calendar is nil in the events controller.
Start by checking the current_customer passes back the customer you were expecting.
Then check that the current_customer has a calendar created for you to reference.

building an order from a cart

i've been having issues trying to build a Cart + Cart/OrderItem + Order combination simple enough yet effective. I've looked around online but i couldn't find anything that fit, so i tried something but .. i'm a bit blocked atm, i don't see how to continue with this. The problem is that i don't know how to get the items in the order and start the cart from scratch (btw it's kind of messy). Also a nice simple tutorial on this would be appreciated as well. Do note i already went through agile web's book example, but for some reason i didn't follow it, it didn't seem to be what i was looking for.
controllers - cart + order
class CartsController < ApplicationController before_filter :initialize_cart
def add
#cart.add_item params[:id]
session["cart"] = #cart.serialize
product = Product.find(params[:id])
redirect_to :back, notice: "Added #{product.name} to cart." end
def show end
def checkout
#order = Order.new user: User.new end end
class OrdersController < ApplicationController
def index
#orders = Order.order('created_at desc').page(params[:page])
end
def show
end
def new
#order = Order.new
end
def create
#order = Order.new(order_params)
respond_to do |format|
if #order.save
format.html { redirect_to root_path, notice:
'Thank you for your order' }
format.json { render action: 'show', status: :created,
location: #order }
else
format.html { render action: 'new' }
format.json { render json: #order.errors,
status: :unprocessable_entity }
end
end
end
private
def set_order
#order = Order.find(params[:id])
end
def order_params
params.require(:order).permit(:pay_type, :user_id)
end
end
now the models
class Order < ActiveRecord::Base
belongs_to :user
PAYMENT_TYPES = [ "Check", "Credit card", "Purchase order" ]
validates :pay_type, inclusion: PAYMENT_TYPES
end
class CartItem
attr_reader :product_id, :quantity
def initialize product_id, quantity = 1
#product_id = product_id
#quantity = quantity
end
def product
Product.find product_id
end
def total_price
product.price * quantity
end
end
class Cart
attr_reader :items
def self.build_from_hash hash
items = if hash["cart"] then
hash["cart"]["items"].map do |item_data|
CartItem.new item_data["product_id"], item_data["quantity"]
end
else
[]
end
new items
end
def initialize items = []
#items = items
end
def add_item product_id
item = #items.find { |item| item.product_id == product_id }
if item
item+=1
else
#items << CartItem.new(product_id)
end
end
def empty?
#items.empty?
end
def count
#items.length
end
def serialize
items = #items.map do |item|
{
"product_id" => item.product_id,
"quantity" => item.quantity
}
end
{
"items" => items
}
end
def total_price
#items.inject(0) { |sum, item| sum + item.total_price }
end
end
Thank you.

Having two objects in shopping at the same time

Two objects at the same time not one add to my shopping cart. Here is the controller and models :
class PublicController < ApplicationController
before_filter :find_or_create_cart, :only => [:add_to_cart, :show_cart]
def list
#products = Product.sorted.paginate(:per_page => 5 , :page => params[:page])
end
def add_to_cart
product = Product.find(params[:id])
#cart.add_product(product)
redirect_to(:action => 'show_cart')
end
def show_cart
end
private
def find_or_create_cart
#cart=session[:cart]||=Cart.new
end
end
AND the model :
class Cart
attr_reader :items
attr_reader :total_price
def initialize
#items = []
#total_price = 0.0
end
def add_product(product)
#items << LineItem.new_based_on(product)
#total_price += product.price
end
end
Model which come from a join table :
class LineItem < ActiveRecord::Base
belongs_to :order
belongs_to :product
def self.new_based_on(product)
line_item = self.new
line_item.product = product
line_item.quantity = 1
line_item.price = product.price
return line_item
end
end
It is a small application which is supposed to do online shopping, but when I hit the button add to cart this will add two objects (same object) to my shopping cart. Hope someone can help me.
I'll write another answer to address your code, but I felt this might help you.
We've developed our own cart system for one of our apps (you can see at http://firststopcosmeticshop.co.uk) we developed based on Ryan Bates' session models Railscast
How It Works
When using sessions to store cart values, you're basically storing a session var with id's from added cart items. Because you want to keep the session as thin as possible, you'll literally just have an array constructed of product id's:
session[:cart][:products] = [] # -> to init
session[:cart][:products] << product.id # -> builds array like [1,5,6,1,6,4,1,5]
This will allow you to then process the cart as you desire (by reading / manipulating the session[:cart][:products] array). If you test the app I referenced, it should help you see it in action
I mentioned your code is inefficient because you're relying on 3 data sources: LineItem, Cart and PublicController. What you really need is CartController and CartSession for a session-based model. This will allow you to call LineItem from your CartController, making it lean
Code
Our session based cart works like this:
#app/models/cart_session.rb
class CartSession
#Initalize Cart Session
def initialize(session)
#session = session
#session[:cart] ||= {}
end
#Cart Count
def cart_count
if (#session[:cart][:products] && #session[:cart][:products] != {})
#session[:cart][:products].count
else
0
end
end
#Cart Contents
def cart_contents
products = #session[:cart][:products]
if (products && products != {})
#Determine Quantities
quantities = Hash[products.uniq.map {|i| [i, products.count(i)]}]
#Get products from DB
products_array = Product.find(products.uniq)
#Create Qty Array
products_new = {}
products_array.each{
|a| products_new[a] = {"qty" => quantities[a.id.to_s]}
}
#Output appended
return products_new
end
end
#Qty & Price Count
def subtotal
products = cart_contents
#Get subtotal of the cart items
subtotal = 0
unless products.blank?
products.each do |a|
subtotal += (a[0]["price"].to_f * a[1]["qty"].to_f)
end
end
return subtotal
end
end
#app/controllers/cart_controller.rb
class CartController < ApplicationController
include ApplicationHelper
#Index
def index
#items = cart_session.cart_contents
#shipping = Shipping.all
end
#Add
def add
session[:cart] ||={}
products = session[:cart][:products]
#If exists, add new, else create new variable
if (products && products != {})
session[:cart][:products] << params[:id]
else
session[:cart][:products] = Array(params[:id])
end
#Handle the request
respond_to do |format|
format.json { render json: cart_session.build_json }
format.html { redirect_to cart_index_path }
end
end
#Delete
def delete
session[:cart] ||={}
products = session[:cart][:products]
id = params[:id]
all = params[:all]
#Is ID present?
unless id.blank?
unless all.blank?
products.delete(params['id'])
else
products.delete_at(products.index(id) || products.length)
end
else
products.delete
end
#Handle the request
respond_to do |format|
format.json { render json: cart_session.build_json }
format.html { redirect_to cart_index_path }
end
end
end
#config/routes.rb
get 'cart' => 'cart#index', :as => 'cart_index'
post 'cart/add/:id' => 'cart#add', :as => 'cart_add'
delete 'cart/remove(/:id(/:all))' => 'cart#delete', :as => 'cart_delete'
This basically allows you to keep a list of people's cart items in a simple session array. The array then gives you the ability to determine the quantity, product type, brand etc, all from the array. It's the most efficient way, and is what I'd recommend for your app
To address your question directly, I believe you need to do the following:
Routes
#config/routes.rb
resources :cart, only: :index do
post :add # -> allows you to add an item
end
Views
#app/views/cart/index.html.erb
<% for product in #products do %>
<%= product.name %>
<%= product.price %>
<% end %>
Controller
class CartController < ApplicationController
respond_to :html, :json, :js
before_action :cart_init, :only => [:add, :index]
#index should replace show_cart
def index
unless #cart.nil?
#products = Product.find #cart #-> will allow you to find multiple ID's
end
respond_with #products
end
#KISS (Keep It Simple)
def add
#cart << params[:id]
respond_with #cart
end
private
def cart_init
#cart = session[:cart][:products] ||= Cart.new #-> keep products in its own key. Allows you to add [:cart][:shipping] etc
end
end
Model
class Cart
attr_reader :items
attr_reader :total
def initialize
#items = session[:cart][:products] ||= []
#total = 0.0
end
end

Resources