merchant_sidekick 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. data/.gitignore +12 -0
  2. data/Changelog.md +38 -0
  3. data/Gemfile +2 -0
  4. data/MIT-LICENSE +19 -0
  5. data/README.md +88 -0
  6. data/Rakefile +10 -0
  7. data/lib/merchant_sidekick.rb +45 -0
  8. data/lib/merchant_sidekick/active_merchant/credit_card_payment.rb +117 -0
  9. data/lib/merchant_sidekick/active_merchant/gateways/authorize_net_gateway.rb +26 -0
  10. data/lib/merchant_sidekick/active_merchant/gateways/base.rb +29 -0
  11. data/lib/merchant_sidekick/active_merchant/gateways/bogus_gateway.rb +19 -0
  12. data/lib/merchant_sidekick/active_merchant/gateways/paypal_gateway.rb +43 -0
  13. data/lib/merchant_sidekick/addressable/address.rb +400 -0
  14. data/lib/merchant_sidekick/addressable/addressable.rb +353 -0
  15. data/lib/merchant_sidekick/buyer.rb +99 -0
  16. data/lib/merchant_sidekick/gateway.rb +81 -0
  17. data/lib/merchant_sidekick/install.rb +19 -0
  18. data/lib/merchant_sidekick/invoice.rb +179 -0
  19. data/lib/merchant_sidekick/line_item.rb +128 -0
  20. data/lib/merchant_sidekick/migrations/addressable.rb +47 -0
  21. data/lib/merchant_sidekick/migrations/billing.rb +100 -0
  22. data/lib/merchant_sidekick/migrations/shopping_cart.rb +28 -0
  23. data/lib/merchant_sidekick/money.rb +38 -0
  24. data/lib/merchant_sidekick/order.rb +244 -0
  25. data/lib/merchant_sidekick/payment.rb +59 -0
  26. data/lib/merchant_sidekick/purchase_invoice.rb +180 -0
  27. data/lib/merchant_sidekick/purchase_order.rb +350 -0
  28. data/lib/merchant_sidekick/railtie.rb +7 -0
  29. data/lib/merchant_sidekick/sales_invoice.rb +56 -0
  30. data/lib/merchant_sidekick/sales_order.rb +122 -0
  31. data/lib/merchant_sidekick/sellable.rb +88 -0
  32. data/lib/merchant_sidekick/seller.rb +93 -0
  33. data/lib/merchant_sidekick/shopping_cart/cart.rb +225 -0
  34. data/lib/merchant_sidekick/shopping_cart/line_item.rb +152 -0
  35. data/lib/merchant_sidekick/version.rb +3 -0
  36. data/merchant_sidekick.gemspec +37 -0
  37. data/spec/address_spec.rb +153 -0
  38. data/spec/addressable_spec.rb +250 -0
  39. data/spec/buyer_spec.rb +203 -0
  40. data/spec/cart_line_item_spec.rb +58 -0
  41. data/spec/cart_spec.rb +213 -0
  42. data/spec/config/merchant_sidekick.yml +10 -0
  43. data/spec/credit_card_payment_spec.rb +175 -0
  44. data/spec/fixtures/addresses.yml +97 -0
  45. data/spec/fixtures/line_items.yml +18 -0
  46. data/spec/fixtures/orders.yml +24 -0
  47. data/spec/fixtures/payments.yml +17 -0
  48. data/spec/fixtures/products.yml +12 -0
  49. data/spec/fixtures/users.yml +11 -0
  50. data/spec/gateway_spec.rb +136 -0
  51. data/spec/invoice_spec.rb +79 -0
  52. data/spec/line_item_spec.rb +65 -0
  53. data/spec/order_spec.rb +85 -0
  54. data/spec/payment_spec.rb +14 -0
  55. data/spec/purchase_invoice_spec.rb +70 -0
  56. data/spec/purchase_order_spec.rb +191 -0
  57. data/spec/sales_invoice_spec.rb +58 -0
  58. data/spec/sales_order_spec.rb +107 -0
  59. data/spec/schema.rb +28 -0
  60. data/spec/sellable_spec.rb +34 -0
  61. data/spec/seller_spec.rb +201 -0
  62. data/spec/spec_helper.rb +255 -0
  63. metadata +201 -0
@@ -0,0 +1,7 @@
1
+ module MerchantSidekick
2
+ class Railtie < ::Rails::Railtie
3
+ generators do
4
+ require File.expand_path("../install.rb", __FILE__)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,56 @@
1
+ # Implements inbound invoices, i.e. the merchant sells a product to a client.
2
+ module MerchantSidekick
3
+ class SalesInvoice < Invoice
4
+ #--- associations
5
+ #has_many :sales_orders, :class_name => "MerchantSidekick::SalesOrder"
6
+
7
+ #--- instance methods
8
+
9
+ def sales_invoice?
10
+ true
11
+ end
12
+
13
+ # cash invoice, combines authorization and capture in one step
14
+ def cash(payment_object, options={})
15
+ transaction do
16
+ cash_result = MerchantSidekick::Payment.class_for(payment_object).transfer(
17
+ gross_total,
18
+ payment_object,
19
+ payment_options(options)
20
+ )
21
+ self.push_payment(cash_result)
22
+
23
+ save(:validate => false)
24
+
25
+ if cash_result.success?
26
+ payment_paid!
27
+ else
28
+ transaction_declined!
29
+ end
30
+ cash_result
31
+ end
32
+ end
33
+
34
+ # returns a hash of additional merchant data passed to authorize
35
+ # you want to pass in the following additional options
36
+ #
37
+ # :ip => ip address of the buyer
38
+ #
39
+ def payment_options(options={})
40
+ { # general
41
+ :buyer => self.buyer,
42
+ :seller => self.seller,
43
+ :payable => self,
44
+ # active merchant relevant
45
+ :customer => "#{self.seller.name} (#{self.seller.id})",
46
+ :email => self.seller && self.seller.respond_to?(:email) ? self.seller.email : nil,
47
+ :invoice => self.number,
48
+ :merchant => self.buyer ? "#{self.buyer.name} (#{self.buyer.id})" : nil,
49
+ :currency => self.currency,
50
+ :billing_address => self.billing_address ? self.billing_address.to_merchant_attributes : nil,
51
+ :shipping_address => self.shipping_address ? self.shipping_address.to_merchant_attributes : nil
52
+ }.merge(options)
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,122 @@
1
+ # Implements inbound orders, i.e. when merchant sells a product.
2
+ module MerchantSidekick
3
+ class SalesOrder < Order
4
+ belongs_to :sales_invoice, :foreign_key => :invoice_id, :class_name => "::MerchantSidekick::SalesInvoice"
5
+
6
+ # Cash the order and generate invoice
7
+ def cash(payment_object, options={})
8
+ defaults = { :order_id => number }
9
+ options = defaults.merge(options).symbolize_keys
10
+
11
+ # before_payment
12
+ seller.send( :before_payment, self ) if seller && seller.respond_to?( :before_payment )
13
+
14
+ self.build_addresses
15
+ self.build_invoice unless self.invoice
16
+
17
+ payment = self.invoice.cash(payment_object, options)
18
+ if payment.success?
19
+ process_payment!
20
+ approve_payment!
21
+ end
22
+
23
+ # after_payment
24
+ buyer.send( :after_payment, self ) if buyer && buyer.respond_to?( :after_payment )
25
+ payment
26
+ end
27
+
28
+ def sales_order?
29
+ true
30
+ end
31
+
32
+ # used in build_invoice to determine which type of invoice
33
+ def to_invoice_class_name
34
+ "MerchantSidekick::SalesInvoice"
35
+ end
36
+
37
+ def invoice
38
+ self.sales_invoice
39
+ end
40
+
41
+ def invoice=(an_invoice)
42
+ self.sales_invoice = an_invoice
43
+ end
44
+
45
+ def build_invoice #:nodoc:
46
+ new_invoice = self.build_sales_invoice(
47
+ :line_items => self.line_items,
48
+ :net_amount => self.net_total,
49
+ :tax_amount => self.tax_total,
50
+ :gross_amount => self.gross_total,
51
+ :buyer => self.buyer,
52
+ :seller => self.seller,
53
+ :origin_address => self.origin_address ? self.origin_address.dup : nil,
54
+ :billing_address => self.billing_address ? self.billing_address.dup : nil,
55
+ :shipping_address => self.shipping_address ? self.shipping_address.dup : nil
56
+ )
57
+
58
+ # set new invoice's line items to invoice we just created
59
+ new_invoice.line_items.each do |li|
60
+ if li.new_record?
61
+ li.invoice = new_invoice
62
+ else
63
+ li.update_attribute(:invoice, new_invoice)
64
+ end
65
+ end
66
+
67
+ # copy addresses
68
+ new_invoice.build_origin_address(self.origin_address.content_attributes) if self.origin_address
69
+ new_invoice.build_billing_address(self.billing_address.content_attributes) if self.billing_address
70
+ new_invoice.build_shipping_address(self.shipping_address.content_attributes) if self.shipping_address
71
+
72
+ self.invoice = new_invoice
73
+
74
+ new_invoice
75
+ end
76
+
77
+ # Builds billing, shipping and origin addresses
78
+ def build_addresses(options={})
79
+ raise ArgumentError.new("No address declared for buyer (#{buyer.class.name} ##{buyer.id}), use acts_as_addressable") \
80
+ unless buyer.respond_to?(:find_default_address)
81
+
82
+ # buyer's billing address
83
+ unless self.default_billing_address
84
+ if buyer.respond_to?(:billing_address) && buyer.default_billing_address
85
+ self.build_billing_address(buyer.default_billing_address.content_attributes)
86
+ else
87
+ if buyer_default_address = buyer.find_default_address
88
+ self.build_billing_address(buyer_default_address.content_attributes)
89
+ else
90
+ raise ArgumentError.new(
91
+ "No billing or default address found for buyer (#{buyer.class.name} ##{buyer.id}), use acts_as_addressable")
92
+ end
93
+ end
94
+ end
95
+
96
+ # buyer's shipping address is optional
97
+ if buyer.respond_to?(:shipping_address)
98
+ self.build_shipping_address(buyer.find_shipping_address_or_clone_from(
99
+ self.billing_address
100
+ ).content_attributes) unless self.default_shipping_address
101
+ end
102
+
103
+ # seller's address for origin address
104
+ raise ArgumentError.new("No address declared for seller (#{seller.class.name} ##{seller.id}), use acts_as_addressable") \
105
+ unless seller.respond_to?(:find_default_address)
106
+
107
+ unless default_origin_address
108
+ if seller.respond_to?(:billing_address) && seller.default_billing_address
109
+ self.build_origin_address(seller.default_billing_address.content_attributes)
110
+ else
111
+ if seller_default_address = seller.find_default_address
112
+ self.build_origin_address(seller_default_address.content_attributes)
113
+ else
114
+ raise ArgumentError.new(
115
+ "No billing or default address found for seller (#{seller.class.name} ##{seller.id}), use acts_as_addressable")
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ end
122
+ end
@@ -0,0 +1,88 @@
1
+ module MerchantSidekick
2
+ module Sellable
3
+ def self.included(mod)
4
+ mod.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+
9
+ # Declares a model as sellable.
10
+ #
11
+ # E.g.
12
+ #
13
+ # class Product < ActiveRecord::Base
14
+ # acts_as_sellable :cents => :price_in_cents, :currency => false
15
+ # end
16
+ #
17
+ # @product.orders.
18
+ #
19
+ def acts_as_sellable(options = {})
20
+ include MerchantSidekick::Sellable::InstanceMethods
21
+ extend MerchantSidekick::Sellable::SingletonMethods
22
+ money :price, options
23
+ has_many :line_items, :as => :sellable, :class_name => "MerchantSidekick::LineItem"
24
+ has_many :orders, :through => :line_items, :class_name => "MerchantSidekick::Order"
25
+ end
26
+ end
27
+
28
+ module SingletonMethods
29
+ def sellable?
30
+ true
31
+ end
32
+ end
33
+
34
+ module InstanceMethods
35
+
36
+ def sellable?
37
+ true
38
+ end
39
+
40
+ # Funny name, but it returns true if the :price represents
41
+ # a gross price including taxes. For that there must be a
42
+ # method called price_is_gross or price_is_gross! as it is
43
+ # in Issue model
44
+ # price_is_net? and/or price_is_gross? should be overwritten
45
+ def price_is_gross?
46
+ false
47
+ end
48
+
49
+ # Opposite of price_is_gross?
50
+ def price_is_net?
51
+ true
52
+ end
53
+
54
+ # This is a product, where the gross and net prices are equal, or in other words
55
+ # a tax for this product is not applicable, e.g. for $10 purchasing credit
56
+ # should be overwritten if otherwise
57
+ def taxable?
58
+ true
59
+ end
60
+
61
+ # There can only be one authorized order per sellable,
62
+ # so the first authorziation is it!
63
+ #
64
+ # Usage:
65
+ # order = issue.settle
66
+ # order.capture unless order.nil?
67
+ # Options:
68
+ # issue.settle :merchant => person
69
+ #
70
+ def settle(options={})
71
+ self.orders.each do |order|
72
+ if order.kind == 'authorization' && current_line_item = order.line_items_find(
73
+ :first, :condition => ["sellable_id => ? AND sellable_type = ?", self.id, self.class.name]
74
+ )
75
+ if adjusted_line_item=order.line_items.build( :order => order, :sellable => self )
76
+ current_line_items.destroy
77
+ order.build_addresses
78
+ order.update
79
+ order.save!
80
+ return order
81
+ end
82
+ end
83
+ end
84
+ nil
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,93 @@
1
+ module MerchantSidekick
2
+ module Seller #:nodoc:
3
+
4
+ def self.included(mod)
5
+ mod.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+
10
+ # Defines helper methods for a person selling items.
11
+ #
12
+ # E.g.
13
+ #
14
+ # class Merchant
15
+ # acts_as_seller
16
+ # ...
17
+ # end
18
+ #
19
+ # # Selling a product to @customer
20
+ # @merchant.sell_to @customer, @products
21
+ #
22
+ # # Alternative syntax
23
+ # @merchant.sell @products, :to => @customer
24
+ #
25
+ def acts_as_seller(options={})
26
+ include MerchantSidekick::Seller::InstanceMethods
27
+ has_many :orders, :as => :seller, :dependent => :destroy, :class_name => "::MerchantSidekick::Order"
28
+ has_many :invoices, :as => :seller, :dependent => :destroy, :class_name => "::MerchantSidekick::Invoice"
29
+ has_many :sales_orders, :as => :seller, :class_name => "::MerchantSidekick::SalesOrder"
30
+ has_many :sales_invoices, :as => :seller, :class_name => "::MerchantSidekick::SalesInvoice"
31
+ end
32
+ end
33
+
34
+ module InstanceMethods
35
+
36
+ def sell_to(buyer, *arguments)
37
+ sell(arguments, :to => buyer)
38
+ end
39
+
40
+ # Sell sellables (line_items) and add them to a sales order
41
+ # The seller will be this person.
42
+ #
43
+ # e.g.
44
+ #
45
+ # seller.sell(@product, :buyer => @buyer)
46
+ #
47
+ def sell(*arguments)
48
+ sellables = []
49
+ options = default_sell_options
50
+
51
+ # distinguish between options and attributes
52
+ arguments = arguments.flatten
53
+ arguments.each do |argument|
54
+ case argument.class.name
55
+ when 'Hash'
56
+ options.merge! argument
57
+ else
58
+ sellables << (argument.is_a?(MerchantSidekick::ShoppingCart::Cart) ? argument.line_items : argument)
59
+ end
60
+ end
61
+ sellables.flatten!
62
+ sellables.reject! {|s| s.blank?}
63
+
64
+ raise ArgumentError.new("No sellable (e.g. product) model provided") if sellables.empty?
65
+ raise ArgumentError.new("Sellable models must have a :price") unless sellables.all? {|sellable| sellable.respond_to? :price}
66
+
67
+ self.sales_orders.build do |so|
68
+ so.buyer = options[:to]
69
+ so.build_addresses
70
+
71
+ sellables.each do |sellable|
72
+ if sellable && sellable.respond_to?(:before_add_to_order)
73
+ sellable.send(:before_add_to_order, self)
74
+ sellable.reload unless sellable.new_record?
75
+ end
76
+ li = LineItem.new(:sellable => sellable, :order => so)
77
+ so.line_items.push(li)
78
+ sellable.send(:after_add_to_order, self) if sellable && sellable.respond_to?(:after_add_to_order)
79
+ end
80
+ self
81
+ end
82
+ end
83
+
84
+ protected
85
+
86
+ # override in model, e.g. :to => @customer
87
+ def default_sell_options
88
+ {}
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,225 @@
1
+ # The Cart class implements a non-persistant shopping cart.
2
+ # It provides methods to add, remove and update "sellable" items. Each sellable
3
+ # item added to the cart is converted to a cart line item. It is recommended
4
+ # to make use of the shopping cart's indirection of purchasing cart line items
5
+ # as products often change there properties, i.e. price, description, etc., as
6
+ # shown in the following example:
7
+ #
8
+ # Purchase w/ cart (recommended) | Simple purchase wo/ cart
9
+ # ------------------------------------+------------------------------------
10
+ # @cart = Cart.new | @order = @buyer.purchase @products
11
+ # @cart.add @products |
12
+ # @order = @buyer.purchase @cart |
13
+ #
14
+ module MerchantSidekick
15
+ module ShoppingCart
16
+ class Cart
17
+ attr_reader :line_items
18
+ attr_reader :currency
19
+ attr_accessor :options
20
+
21
+ def initialize(currency_code = 'USD', options = {})
22
+ @currency = currency_code
23
+ @options = {:currency_code => currency_code}.merge(options)
24
+ empty!
25
+ end
26
+
27
+ # Adds a single or array of sellable products to the cart and
28
+ # returns the cart line items.
29
+ #
30
+ # E.g.
31
+ #
32
+ # @cart.add @sellable
33
+ # @cart.add @sellable, 4
34
+ # @cart.add [@sellable1, @sellable2]
35
+ #
36
+ def add(stuff, quantity = 1, options = {})
37
+ if stuff.is_a?(Array)
38
+ stuff.inject([]) {|result, element| result << add(element, quantity, options)}
39
+ elsif stuff.is_a?(MerchantSidekick::ShoppingCart::LineItem)
40
+ self.add_cart_line_item(stuff, options)
41
+ else
42
+ # assuming it is a "product" (e.g. sellable) instance
43
+ self.add_product(stuff, quantity, options)
44
+ end
45
+ end
46
+
47
+ # Removes an item from the cart
48
+ def remove(stuff, options={})
49
+ if stuff.is_a?(MerchantSidekick::ShoppingCart::LineItem)
50
+ self.remove_cart_line_item(stuff, options)
51
+ else
52
+ self.remove_product(stuff, options)
53
+ end
54
+ end
55
+ alias_method :delete, :remove
56
+
57
+ # Updates an existing line_item with quantity by product or line_item
58
+ # instance. If quanity is <= 0, the item will be removed.
59
+ def update(stuff, quantity, options={})
60
+ if stuff.is_a?(MerchantSidekick::ShoppingCart::LineItem)
61
+ self.update_cart_line_item(stuff, quantity, options)
62
+ else
63
+ self.update_product(stuff, quantity, options)
64
+ end
65
+ end
66
+
67
+ # Finds an instance of line item, by product or line_item
68
+ #
69
+ # E.g.
70
+ #
71
+ # @cart.find(:first, @product) # -> @li
72
+ # @cart.find(:all, @product) # -> [@li1, @li2]
73
+ #
74
+ def find(what, stuff, options={})
75
+ if stuff.is_a?(MerchantSidekick::ShoppingCart::LineItem)
76
+ self.find_line_items(what, stuff, options)
77
+ else
78
+ self.find_line_items_by_product(what, stuff, options)
79
+ end
80
+ end
81
+
82
+ # Remove all line items from cart
83
+ def empty!
84
+ @line_items = []
85
+ end
86
+
87
+ # Check to see if cart is empty?
88
+ def empty?
89
+ @line_items.empty?
90
+ end
91
+
92
+ # Evaluates the total amount of a sum as all item prices * quantity
93
+ def total_amount
94
+ sum = ::Money.new(1, self.currency)
95
+ @line_items.each {|li| sum += li.total_amount}
96
+ sum -= ::Money.new(1, self.currency)
97
+ sum
98
+ end
99
+ alias_method :total, :total_amount
100
+
101
+ # counts number of line items.
102
+ def line_items_count
103
+ self.line_items.size
104
+ end
105
+
106
+ # counts number of entities line_items * quantities.
107
+ def items_count
108
+ counter = 0
109
+ self.line_items.each do |item|
110
+ counter += item.quantity
111
+ end
112
+ counter
113
+ end
114
+
115
+ # Create a product line from a product (sellable) and copies
116
+ # all attributes that could be modified later
117
+ def cart_line_item(product, quantity = 1, line_options = {})
118
+ raise "No price column available for '#{product.class.name}'" unless product.respond_to?(:price)
119
+ # we need to set currency explicitly here for correct money conversion of the cart_line_item
120
+ MerchantSidekick::ShoppingCart::LineItem.new do |line_item|
121
+ line_item.options = self.options.merge(line_options)
122
+ line_item.currency = self.currency
123
+ line_item.quantity = quantity
124
+ line_item.product = product
125
+ line_item
126
+ end
127
+ end
128
+
129
+ # Return a list of cart line items from an array of products, e.g. Products
130
+ def cart_line_items(products)
131
+ products.map {|p| self.cart_line_item(p)}
132
+ end
133
+
134
+ # cart options setter
135
+ def options=(some_options = {})
136
+ @options = some_options.to_hash
137
+ end
138
+
139
+ # cart options getter
140
+ def options
141
+ @options || {}
142
+ end
143
+
144
+ protected
145
+
146
+ # Add product line
147
+ # Returns cart total price
148
+ def add_cart_line_item(newitem, options={})
149
+ return nil if newitem.nil?
150
+ item = find(:first, newitem)
151
+ if item
152
+ # existing item found, update item quantity and add total_price
153
+ item.quantity += newitem.quantity
154
+ else
155
+ # not in cart yet
156
+ item = newitem
157
+ @line_items << item
158
+ end
159
+ item
160
+ end
161
+
162
+ # Add purchasable, which most likely will be a product
163
+ # Returns the total price
164
+ def add_product(a_product, quantity=1, options={})
165
+ return nil if a_product.nil?
166
+ item = find(:first, a_product)
167
+ if item
168
+ item.quantity += quantity
169
+ else
170
+ item = self.cart_line_item(a_product, quantity, options)
171
+ @line_items << item
172
+ end
173
+ item
174
+ end
175
+
176
+ # Remove a product line and adjust the total price
177
+ def remove_cart_line_item(a_cart_line_item, options={})
178
+ deleted_line_item = nil
179
+ item_to_remove = find(:first, a_cart_line_item)
180
+ deleted_line_item = @line_items.delete(item_to_remove) if item_to_remove
181
+ deleted_line_item
182
+ end
183
+
184
+ # Remove a purchasable and adjust the total price
185
+ def remove_product(a_product, options={})
186
+ deleted_line_item = nil
187
+ item_to_remove = find(:first, a_product)
188
+ deleted_line_item = @line_items.delete(item_to_remove) if item_to_remove
189
+ deleted_line_item
190
+ end
191
+
192
+ # updates quantity by line item
193
+ def update_cart_line_item(a_line_item, quantity, options={})
194
+ return remove(a_line_item, options) if quantity <= 0
195
+ item = find(:first, a_line_item)
196
+ item.quantity = quantity if item
197
+ item
198
+ end
199
+
200
+ # updates quantity py product
201
+ def update_product(a_product, quantity, options={})
202
+ return remove(a_product, options) if quantity <= 0
203
+ item = find(:first, a_product)
204
+ item.quantity = quantity if item
205
+ item
206
+ end
207
+
208
+ def find_line_items(what, a_cart_line_item, options={})
209
+ if :all == what
210
+ @line_items.select { |i| i.product_id == a_cart_line_item.product.id && i.product_type == a_cart_line_item.product.class.base_class.name }
211
+ elsif :first == what
212
+ @line_items.find { |i| i.product_id == a_cart_line_item.product.id && i.product_type == a_cart_line_item.product.class.base_class.name }
213
+ end
214
+ end
215
+
216
+ def find_line_items_by_product(what, a_product, options={})
217
+ if :all == what
218
+ @line_items.select { |i| i.product_id == a_product.id && i.product_type == a_product.class.base_class.name }
219
+ elsif :first == what
220
+ @line_items.find { |i| i.product_id == a_product.id && i.product_type == a_product.class.base_class.name }
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end