-
Notifications
You must be signed in to change notification settings - Fork 409
/
cart.rb
272 lines (246 loc) · 11.4 KB
/
cart.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# the cart is simply a bucket of stuff. it is very well documented in a blog post I have posted:
# The first thing that comes to mind of a great cart is that it is "stupid".
# Sure that might sound crazy but it is very true. A shopping cart should not know much
# about your "order" or your "products". Your cart should simply be a bucket to keep a
# bunch of products for a specific user. It doesn't know the price of the products or
# anything about the checkout process.
#
# The second thing about a great cart is that it ironically it is "smart" about itself.
# Your cart should NOT know about the order or products but it should know about itself.
# Note your cart is the combination of a "cart model" and "cart_items model". This cart_items
# model has the following fields:
#
# cart_id
# product_id
# item_type_id
# quantity
# Additionally the cart should only have the following field:
#
# user_id (this is the stylist user_id in the admin area)
# customer_id (for the customer facing site this == user_id)
#
#
# Also note I am in favor of adding user_id to the cart_items table to keep queries more simple.
#
# One feature I added to the minimum model is the field item_type_id. item_type_id refers to a
# model that just has a name field. The ItemTypes are as follows:
#
# shopping_cart
# save_for_later
# wish_list
# purchased
# deleted
# admin_cart
#
# What this simple field in the DB does is allows you to have a wish list and save for later
# functionality out of the box. Additionally you can WOW your marketing team by telling them
# all the items a user has ever deleted out of their cart or purchased. If you ever need to create
# a recommendation engine your cart will give you all the data you need.
#
# Now with this model, purchasing an item is simply taking the items that are in the cart and
# have an item_type of shopping_cart and moving them to an order / order_items object. Once
# you purchase an order_item you change the cart_item.item_type to "purchased" with cart.mark_items_purchased(order)
# _______________________
# == Combining the Cart and Order Objects
#
# I've heard the argument that using an order object for the cart "make things easier".
# Not only do I disagree but sorry, "You would be wrong". By mixing the cart and the order you have not
# separated concerns. This can make validations very conditional. It also mixes cart logic with order logic.
#
# I view your cart as something that can be removed off the face of the planet and not effect much. Sure
# people would be upset to add things back to their cart but at the end of the day it would not effect anything
# financially. The order however is sacred. If an order was deleted you could lose financial data and even
# fulfillment information. Hence you don't want to be messing around with the order because you could be
# shooting yourself in the foot.
#
# By nature your cart has a lot of abandoned records. If the order and cart are separated you could very easily
# archive the carts without much worry. If your order is your cart the risk to do this would be too great.
# One small bug could cost you way too much.
#
# Now you have an extremely slim cart with a tremendous amount of functionality.
# == Removing an item
#
# The when the item is removed from the cart instead of deleting the cart_item the item is
# changed to active = false. This allows you to see the state of the item when it was removed from the cart.
#
# == Save for later
#
# The when the item is marked for "save_for_later" in the cart now the state of the item is just changed to "save_for_later".
# So adding it back to the cart is as easy as changing the state to "shopping_cart".
#
# Take a look at [This Blog Post](http://www.ror-e.com/posts/29-e-commerce-tips-1-2-the-shopping-cart) for more details.
# == Schema Information
#
# Table name: carts
#
# id :integer(4) not null, primary key
# user_id :integer(4)
# created_at :datetime
# updated_at :datetime
# customer_id :integer(4)
#
class Cart < ApplicationRecord
belongs_to :user
belongs_to :customer, class_name: 'User'
has_many :cart_items
has_many :shopping_cart_items, -> { where(active: true, item_type_id: ItemType::SHOPPING_CART_ID) }, class_name: 'CartItem'
has_many :saved_cart_items, -> { where( active: true, item_type_id: ItemType::SAVE_FOR_LATER_ID) }, class_name: 'CartItem'
has_many :wish_list_items, -> { where( active: true, item_type_id: ItemType::WISH_LIST_ID) }, class_name: 'CartItem'
has_many :purchased_items, -> { where( active: true, item_type_id: ItemType::PURCHASED_ID) }, class_name: 'CartItem'
has_many :deleted_cart_items, -> { where(active: false) }, class_name: 'CartItem'
accepts_nested_attributes_for :shopping_cart_items
# Adds all the item prices (not including taxes) that are currently in the shopping cart
#
# @param [none]
# @return [Float] This is a float in decimal form and represents the price of all the items in the cart
def sub_total
shopping_cart_items.map(&:total).sum
end
# Adds the quantity of items that are currently in the shopping cart
#
# @param [none]
# @return [Integer] Quantity all the items in the cart
def number_of_shopping_cart_items
shopping_cart_items.map(&:quantity).sum
end
# Call this method when you are checking out with the current cart items
# => these will now be order.order_items
# => the order can only add items if it is 'in_progress'
#
# @param [Order] order to insert the shopping cart variants into
# @return [order] return order because teh order returned has a diffent quantity
def add_items_to_checkout(order)
if order.in_progress?
order.order_items.map(&:destroy)
order.order_items.reload
items_to_add(order, shopping_cart_items)
end
order
end
def shopping_cart_items_equal_order_items?(order)
# cart item has quantity, but order item doesn't.
# for example: cart and order both have two of the same item which variant_id is 1
# cart will look like this: [{variant_id: 1, quantity: 2}]
# order will look like this: [{variant_id: 1}, {variant_id: 1}]
# before comparing, both need to be converted into the same style
variant_ids_in_cart = []
shopping_cart_items.each do |item|
item.quantity.times do
variant_ids_in_cart.push(item.variant_id)
end
end
order_items = order.order_items.map(&:variant_id)
variant_ids_in_cart.sort == order_items.sort
end
# Call this method when you want to add an item to the shopping cart
#
# @param [Integer, #read] variant id to add to the cart
# @param [User, #read] user that is adding something to the cart
# @param [Integer, #optional] ItemType id that is being added to the cart
# @return [CartItem] return the cart item that is added to the cart
def add_variant(variant_id, customer, qty = 1, cart_item_type_id = ItemType::SHOPPING_CART_ID, admin_purchase = false)
items = shopping_cart_items.where(variant_id: variant_id).to_a
variant = Variant.find_by(id: variant_id)
quantity_to_purchase = variant.quantity_purchaseable_if_user_wants(qty.to_i, admin_purchase)
if admin_purchase && (quantity_to_purchase > 0)
cart_item = add_cart_items(items, quantity_to_purchase, customer, cart_item_type_id, variant_id)
elsif variant.sold_out?
cart_item = saved_cart_items.create(variant_id: variant_id,
user: customer,
item_type_id: ItemType::SAVE_FOR_LATER_ID,
quantity: qty
) if items.size < 1
else
cart_item = add_cart_items(items, quantity_to_purchase, customer, cart_item_type_id, variant_id)
end
cart_item
end
# Call this method when you want to remove an item from the shopping cart
# The CartItem will not delete. Instead it is just inactivated
#
# @param [Integer, #read] variant id to add to the cart
# @return [CartItem] return the cart item that is added to the cart
def remove_variant(variant_id)
citems = self.cart_items.each {|ci| ci.inactivate! if variant_id.to_i == ci.variant_id }
return citems
end
# Call this method when you want to associate the cart with a user
#
# @param [User]
def save_user(u) # u is user object or nil
if u && self.user_id != u.id
self.user_id = u.id
self.save
end
end
# Call this method when you want to mark the items in the order as purchased
# The CartItem will not delete. Instead the item_type changes to purchased
#
# @param [Order]
def mark_items_purchased(order)
CartItem.where(id: (self.cart_item_ids + self.shopping_cart_item_ids).uniq).
where(variant_id: order.variant_ids).
update_all("item_type_id = #{ItemType::PURCHASED_ID}") if !order.variant_ids.empty?
end
def merge_with_previous_cart!
if user_id && previous_cart
current_items = cart_items.map(&:variant_id)
previous_cart.cart_items.each do |item|
self.add_variant(item.variant_id, item.user, item.quantity) unless current_items.include?(item.variant_id)
end
end
end
def self.previous_for_user(cart_id, user_id)
Cart.where(['id <> ?', cart_id]).where(user_id: user_id).last
end
private
def previous_cart
@previous_cart ||= Cart.previous_for_user(id, user_id)
end
def update_shopping_cart(cart_item,customer, qty = 1)
if customer
self.shopping_cart_items.find(cart_item.id).update_attributes(:quantity => (cart_item.quantity + qty), :user_id => customer.id)
else
self.shopping_cart_items.find(cart_item.id).update_attributes(:quantity => (cart_item.quantity + qty))
end
end
def add_cart_items(items, qty, customer, cart_item_type_id, variant_id)
if items.size < 1
cart_item = shopping_cart_items.create(variant_id: variant_id,
user: customer,
item_type_id: cart_item_type_id,
quantity: qty
)
else
cart_item = items.first
update_shopping_cart(cart_item,customer, qty)
end
cart_item
end
def items_to_add(order, items)
items.each do |item|
order.add_cart_item( item, nil)
end
end
def items_to_add_or_destroy(items_in_cart, order)
#destroy_any_order_item_that_was_removed_from_cart
destroy_order_items_not_in_cart!(items_in_cart, order)
# order.order_items.delete_all #destroy(order_item.id)
items = order.order_items.inject({}) {|h, item| h[item.variant_id].nil? ? h[item.variant_id] = [item.id] : h[item.variant_id] << item.id; h}
items_in_cart.each_pair do |variant_id, qty_in_cart|
variant = Variant.find(variant_id)
if items[variant_id].nil? # the order does not have any order_items with this variant_id
order.add_items( variant , qty_in_cart)
elsif qty_in_cart - items[variant_id].size > 0 # the order does not enough order_items with this variant_id
order.add_items( variant , qty_in_cart - items[variant_id].size)
elsif qty_in_cart - items[variant_id].size < 0 # the order has too many order_items with this variant_id
order.remove_items( variant , qty_in_cart )
end
end
order
end
private
def destroy_order_items_not_in_cart!(items_in_cart, order)
order.order_items.delete_if {|order_item| !items_in_cart.keys.any?{|variant_id| variant_id == order_item.variant_id } }
end
end